feat(aio): code snippet source available & shown when code missing

Tells reader (usually the author) what code file is missing
Also when no linenums specified, turn them on if num of lines > 10
This commit is contained in:
Ward Bell 2017-04-27 22:57:34 -07:00 committed by Pete Bacon Darwin
parent f1f04fa782
commit 7b94f493b9
7 changed files with 122 additions and 60 deletions

View File

@ -81,6 +81,8 @@ class TestCodeComponent {
@Input() code = ''; @Input() code = '';
@Input() language: string; @Input() language: string;
@Input() linenums: boolean | number; @Input() linenums: boolean | number;
@Input() path: string;
@Input() region: string;
get someCode() { get someCode() {
return this.code && this.code.length > 30 ? this.code.substr(0, 30) + '...' : this.code; return this.code && this.code.length > 30 ? this.code.substr(0, 30) + '...' : this.code;

View File

@ -16,7 +16,8 @@ import { Component, ElementRef, OnInit } from '@angular/core';
selector: 'code-example', selector: 'code-example',
template: ` template: `
<header *ngIf="title">{{title}}</header> <header *ngIf="title">{{title}}</header>
<aio-code [ngClass]="{'headed-code':title, 'simple-code':!title}" [code]="code" [language]="language" [linenums]="linenums"></aio-code> <aio-code [ngClass]="{'headed-code':title, 'simple-code':!title}" [code]="code"
[language]="language" [linenums]="linenums" [path]="path" [region]="region"></aio-code>
` `
}) })
export class CodeExampleComponent implements OnInit { export class CodeExampleComponent implements OnInit {
@ -24,12 +25,17 @@ export class CodeExampleComponent implements OnInit {
code: string; code: string;
language: string; language: string;
linenums: boolean | number; linenums: boolean | number;
path: string;
region: string;
title: string; title: string;
constructor(private elementRef: ElementRef) { constructor(private elementRef: ElementRef) {
const element = this.elementRef.nativeElement; const element = this.elementRef.nativeElement;
this.language = element.getAttribute('language') || ''; this.language = element.getAttribute('language') || '';
this.linenums = element.getAttribute('linenums'); this.linenums = element.getAttribute('linenums');
this.path = element.getAttribute('path') || '';
this.region = element.getAttribute('region') || '';
this.title = element.getAttribute('title') || ''; this.title = element.getAttribute('title') || '';
} }

View File

@ -2,9 +2,13 @@
import { Component, ElementRef, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit } from '@angular/core';
export interface TabInfo { export interface TabInfo {
title: string; class: string;
language: string;
code: string; code: string;
language: string;
linenums: any;
path: string;
region: string;
title: string;
} }
/** /**
@ -17,14 +21,15 @@ export interface TabInfo {
@Component({ @Component({
selector: 'code-tabs', selector: 'code-tabs',
template: ` template: `
<md-tab-group class="code-tab-group"> <md-tab-group class="code-tab-group">
<md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs"> <md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
<ng-template md-tab-label> <ng-template md-tab-label>
<span class="{{tab.class}}">{{ tab.title }}</span> <span class="{{tab.class}}">{{ tab.title }}</span>
</ng-template> </ng-template>
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums" class="{{ tab.class }}"></aio-code> <aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums"
</md-tab> [path]="tab.path" [region]="tab.region" class="{{ tab.class }}"></aio-code>
</md-tab-group> </md-tab>
</md-tab-group>
` `
}) })
export class CodeTabsComponent implements OnInit { export class CodeTabsComponent implements OnInit {
@ -59,6 +64,8 @@ export class CodeTabsComponent implements OnInit {
class: codeExample.getAttribute('class'), class: codeExample.getAttribute('class'),
language: codeExample.getAttribute('language'), language: codeExample.getAttribute('language'),
linenums: this.getLinenums(codeExample), linenums: this.getLinenums(codeExample),
path: codeExample.getAttribute('path') || '',
region: codeExample.getAttribute('region') || '',
title: codeExample.getAttribute('title') title: codeExample.getAttribute('title')
}; };
this.tabs.push(tab); this.tabs.push(tab);

View File

@ -12,7 +12,7 @@ import { PrettyPrinter } from './pretty-printer.service';
const oneLineCode = 'const foo = "bar";'; const oneLineCode = 'const foo = "bar";';
const multiLineCode = ` const smallMultiLineCode = `
&lt;hero-details&gt; &lt;hero-details&gt;
&lt;h2&gt;Bah Dah Bing&lt;/h2&gt; &lt;h2&gt;Bah Dah Bing&lt;/h2&gt;
&lt;hero-team&gt; &lt;hero-team&gt;
@ -20,6 +20,8 @@ const multiLineCode = `
&lt;/hero-team&gt; &lt;/hero-team&gt;
&lt;/hero-details&gt;`; &lt;/hero-details&gt;`;
const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode;
describe('CodeComponent', () => { describe('CodeComponent', () => {
let codeComponentDe: DebugElement; let codeComponentDe: DebugElement;
let codeComponent: CodeComponent; let codeComponent: CodeComponent;
@ -75,38 +77,38 @@ describe('CodeComponent', () => {
expect(spans.length).toBeGreaterThan(0, 'formatted spans'); expect(spans.length).toBeGreaterThan(0, 'formatted spans');
}); });
function hasLineNumbers() {
// presence of `<li>`s are a tell-tale for line numbers
return 0 < codeComponentDe.nativeElement.querySelectorAll('li').length;
}
it('should format a one-line code sample without linenums by default', () => { it('should format a one-line code sample without linenums by default', () => {
// `<li>`s are a tell-tale for line numbers expect(hasLineNumbers()).toBe(false);
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
expect(lis.length).toBe(0, 'should be no linenums');
}); });
it('should add line numbers to one-line code sample when linenums set true', () => { it('should add line numbers to one-line code sample when linenums set true', () => {
hostComponent.linenums = 'true'; hostComponent.linenums = 'true';
fixture.detectChanges(); fixture.detectChanges();
expect(hasLineNumbers()).toBe(true);
// `<li>`s are a tell-tale for line numbers
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
expect(lis.length).toBe(1, 'has linenums');
}); });
it('should format multi-line code with linenums by default', () => { it('should format a small multi-line code without linenums by default', () => {
hostComponent.code = multiLineCode; hostComponent.code = smallMultiLineCode;
fixture.detectChanges(); fixture.detectChanges();
expect(hasLineNumbers()).toBe(false);
// `<li>`s are a tell-tale for line numbers
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
expect(lis.length).toBeGreaterThan(0, 'has linenums');
}); });
it('should not format multi-line code when linenums set false', () => { it('should add line numbers to a big multi-line code by default', () => {
hostComponent.code = bigMultiLineCode;
fixture.detectChanges();
expect(hasLineNumbers()).toBe(true);
});
it('should format big multi-line code without linenums when linenums set false', () => {
hostComponent.linenums = false; hostComponent.linenums = false;
hostComponent.code = multiLineCode; hostComponent.code = bigMultiLineCode;
fixture.detectChanges(); fixture.detectChanges();
expect(hasLineNumbers()).toBe(false);
// `<li>`s are a tell-tale for line numbers
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
expect(lis.length).toBe(0, 'should be no linenums');
}); });
}); });
@ -121,7 +123,7 @@ describe('CodeComponent', () => {
it('should trim whitespace from the code before rendering', () => { it('should trim whitespace from the code before rendering', () => {
hostComponent.linenums = false; hostComponent.linenums = false;
hostComponent.code = '\n\n\n' + multiLineCode + '\n\n\n'; hostComponent.code = '\n\n\n' + smallMultiLineCode + '\n\n\n';
fixture.detectChanges(); fixture.detectChanges();
const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText; const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText;
expect(codeContent).toEqual(codeContent.trim()); expect(codeContent).toEqual(codeContent.trim());
@ -137,18 +139,42 @@ describe('CodeComponent', () => {
}); });
describe('error message', () => { describe('error message', () => {
it('should display error message when there is no code (after trimming)', () => {
hostComponent.code = ' \n '; function getErrorMessage() {
fixture.detectChanges(); const missing: HTMLElement = codeComponentDe.nativeElement.querySelector('.code-missing');
const missing = codeComponentDe.nativeElement.querySelector('.code-missing') as HTMLElement; return missing ? missing.innerText : null;
expect(missing).not.toBeNull('should have element with "code-missing" class'); }
expect(missing.innerText).toContain('missing', 'error message');
});
it('should not display "code-missing" class when there is some code', () => { it('should not display "code-missing" class when there is some code', () => {
fixture.detectChanges(); fixture.detectChanges();
const missing = codeComponentDe.nativeElement.querySelector('.code-missing'); expect(getErrorMessage()).toBeNull('should not have element with "code-missing" class');
expect(missing).toBeNull('should not have element with "code-missing" class'); });
it('should display error message when there is no code (after trimming)', () => {
hostComponent.code = ' \n ';
fixture.detectChanges();
expect(getErrorMessage()).toContain('missing');
});
it('should show path and region in missing-code error message', () => {
hostComponent.code = ' \n ';
hostComponent.path = 'fizz/buzz/foo.html';
hostComponent.region = 'something';
fixture.detectChanges();
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html#something$/);
});
it('should show path only in missing-code error message when no region', () => {
hostComponent.code = ' \n ';
hostComponent.path = 'fizz/buzz/foo.html';
fixture.detectChanges();
expect(getErrorMessage()).toMatch(/for[\s\S]fizz\/buzz\/foo\.html$/);
});
it('should show simple missing-code error message when no path/region', () => {
hostComponent.code = ' \n ';
fixture.detectChanges();
expect(getErrorMessage()).toMatch(/missing.$/);
}); });
}); });
@ -197,13 +223,16 @@ describe('CodeComponent', () => {
@Component({ @Component({
selector: 'aio-host-comp', selector: 'aio-host-comp',
template: ` template: `
<aio-code md-no-ink [code]="code" [language]="language" [linenums]="linenums"></aio-code> <aio-code md-no-ink [code]="code" [language]="language"
[linenums]="linenums" [path]="path" [region]="region"></aio-code>
` `
}) })
class HostComponent { class HostComponent {
code = oneLineCode; code = oneLineCode;
language: string; language: string;
linenums: boolean | number | string; linenums: boolean | number | string;
path: string;
region: string;
} }
class TestLogger { class TestLogger {

View File

@ -6,6 +6,7 @@ import { MdSnackBar } from '@angular/material';
const originalLabel = 'Copy Code'; const originalLabel = 'Copy Code';
const copiedLabel = 'Copied!'; const copiedLabel = 'Copied!';
const defaultLineNumsCount = 10; // by default, show linenums over this number
/** /**
* Formatted Code Block * Formatted Code Block
@ -17,14 +18,18 @@ const copiedLabel = 'Copied!';
* Example usage: * Example usage:
* *
* ``` * ```
* <aio-code [code]="variableContainingCode" [language]="ts" [linenums]="true"></aio-code> * <aio-code
* [code]="variableContainingCode"
* [language]="ts"
* [linenums]="true"
* [path]="ts-to-js/ts/src/app/app.module.ts"
* [region]="ng2import">
* </aio-code>
* ``` * ```
*
*/ */
@Component({ @Component({
selector: 'aio-code', selector: 'aio-code',
template: ` template: `
<pre class="prettyprint lang-{{language}}"> <pre class="prettyprint lang-{{language}}">
<button *ngIf="code" class="material-icons copy-button" (click)="doCopy()">content_copy</button> <button *ngIf="code" class="material-icons copy-button" (click)="doCopy()">content_copy</button>
<code class="animated fadeIn" #codeContainer></code> <code class="animated fadeIn" #codeContainer></code>
@ -33,6 +38,12 @@ const copiedLabel = 'Copied!';
}) })
export class CodeComponent implements OnChanges { export class CodeComponent implements OnChanges {
/**
* The code to be formatted, this should already be HTML encoded
*/
@Input()
code: string;
/** /**
* The language of the code to render * The language of the code to render
* (could be javascript, dart, typescript, etc) * (could be javascript, dart, typescript, etc)
@ -50,10 +61,16 @@ export class CodeComponent implements OnChanges {
linenums: boolean | number | string; linenums: boolean | number | string;
/** /**
* The code to be formatted, this should already be HTML encoded * path to the source of the code being displayed
*/ */
@Input() @Input()
code: string; path: string;
/**
* region of the source of the code being displayed
*/
@Input()
region: string;
/** /**
* The element in the template that will display the formatted code * The element in the template that will display the formatted code
@ -70,7 +87,9 @@ export class CodeComponent implements OnChanges {
this.code = this.code && leftAlign(this.code); this.code = this.code && leftAlign(this.code);
if (!this.code) { if (!this.code) {
this.setCodeHtml('<p class="code-missing">The code sample is missing.</p>'); const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
const srcMsg = src ? ` for<br>${src}` : '.';
this.setCodeHtml(`<p class="code-missing">The code sample is missing${srcMsg}</p>`);
return; return;
} }
@ -117,7 +136,7 @@ export class CodeComponent implements OnChanges {
// if no linenums, enable line numbers if more than one line // if no linenums, enable line numbers if more than one line
return linenums == null || linenums === NaN ? return linenums == null || linenums === NaN ?
(this.code.match(/\n/g) || []).length > 1 : linenums; (this.code.match(/\n/g) || []).length > defaultLineNumsCount : linenums;
} }
} }

View File

@ -17,8 +17,6 @@ module.exports = function renderExamples(getExampleRegion) {
if (attrMap.path) { if (attrMap.path) {
// We found a path attribute so look up the example and rebuild the HTML // We found a path attribute so look up the example and rebuild the HTML
const exampleContent = getExampleRegion(doc, attrMap.path, attrMap.region); const exampleContent = getExampleRegion(doc, attrMap.path, attrMap.region);
delete attrMap.path;
delete attrMap.region;
attributes = Object.keys(attrMap).map(key => ` ${key}="${attrMap[key].replace(/"/g, '&quot;')}"`).join(''); attributes = Object.keys(attrMap).map(key => ` ${key}="${attrMap[key].replace(/"/g, '&quot;')}"`).join('');
return `<${element}${attributes}>\n${exampleContent}\n</${element}>`; return `<${element}${attributes}>\n${exampleContent}\n</${element}>`;
} }

View File

@ -45,7 +45,7 @@ describe('renderExamples processor', () => {
{ renderedContent: `<${CODE_TAG} path="test/url">Some code</${CODE_TAG}>`} { renderedContent: `<${CODE_TAG} path="test/url">Some code</${CODE_TAG}>`}
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url">\nwhole file\n</${CODE_TAG}>`);
}); });
it(`should replace all instances of <${CODE_TAG}> tags`, () => { it(`should replace all instances of <${CODE_TAG}> tags`, () => {
@ -53,7 +53,7 @@ describe('renderExamples processor', () => {
{ renderedContent: `<${CODE_TAG} path="test/url">Some code</${CODE_TAG}><${CODE_TAG} path="test/url" region="region-1">Other code</${CODE_TAG}>`} { renderedContent: `<${CODE_TAG} path="test/url">Some code</${CODE_TAG}><${CODE_TAG} path="test/url" region="region-1">Other code</${CODE_TAG}>`}
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n</${CODE_TAG}><${CODE_TAG}>\nregion 1 contents\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url">\nwhole file\n</${CODE_TAG}><${CODE_TAG} path="test/url" region="region-1">\nregion 1 contents\n</${CODE_TAG}>`);
}); });
it('should contain the region contents from the example file if a region is specified', () => { it('should contain the region contents from the example file if a region is specified', () => {
@ -61,7 +61,7 @@ describe('renderExamples processor', () => {
{ renderedContent: `<${CODE_TAG} path="test/url" region="region-1">Some code</${CODE_TAG}>` } { renderedContent: `<${CODE_TAG} path="test/url" region="region-1">Some code</${CODE_TAG}>` }
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nregion 1 contents\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url" region="region-1">\nregion 1 contents\n</${CODE_TAG}>`);
}); });
it(`should replace the content of the <${CODE_TAG}> tag with the whole contents from an example file if the region is empty`, () => { it(`should replace the content of the <${CODE_TAG}> tag with the whole contents from an example file if the region is empty`, () => {
@ -69,15 +69,16 @@ describe('renderExamples processor', () => {
{ renderedContent: `<${CODE_TAG} path="test/url" region="">Some code</${CODE_TAG}>` } { renderedContent: `<${CODE_TAG} path="test/url" region="">Some code</${CODE_TAG}>` }
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url" region="">\nwhole file\n</${CODE_TAG}>`);
}); });
it('should remove the path and region attributes but leave the other attributes alone', () => { it('should pass along all attributes including path and region', () => {
const docs = [ const openTag = `<${CODE_TAG} class="special" path="test/url" linenums="15" region="region-1" id="some-id">`;
{ renderedContent: `<${CODE_TAG} class="special" path="test/url" linenums="15" region="region-1" id="some-id">Some code</${CODE_TAG}>` }
const docs = [ { renderedContent: `${openTag}Some code</${CODE_TAG}>` }
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} class="special" linenums="15" id="some-id">\nregion 1 contents\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`${openTag}\nregion 1 contents\n</${CODE_TAG}>`);
}); });
it('should cope with spaces and double quotes inside attribute values', () => { it('should cope with spaces and double quotes inside attribute values', () => {
@ -85,7 +86,7 @@ describe('renderExamples processor', () => {
{ renderedContent: `<${CODE_TAG} title='a "quoted" value' path="test/url"></${CODE_TAG}>`} { renderedContent: `<${CODE_TAG} title='a "quoted" value' path="test/url"></${CODE_TAG}>`}
]; ];
processor.$process(docs); processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a &quot;quoted&quot; value">\nwhole file\n</${CODE_TAG}>`); expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a &quot;quoted&quot; value" path="test/url">\nwhole file\n</${CODE_TAG}>`);
}); });
}) })
); );