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:
parent
f1f04fa782
commit
7b94f493b9
|
@ -81,6 +81,8 @@ class TestCodeComponent {
|
|||
@Input() code = '';
|
||||
@Input() language: string;
|
||||
@Input() linenums: boolean | number;
|
||||
@Input() path: string;
|
||||
@Input() region: string;
|
||||
|
||||
get someCode() {
|
||||
return this.code && this.code.length > 30 ? this.code.substr(0, 30) + '...' : this.code;
|
||||
|
|
|
@ -16,7 +16,8 @@ import { Component, ElementRef, OnInit } from '@angular/core';
|
|||
selector: 'code-example',
|
||||
template: `
|
||||
<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 {
|
||||
|
@ -24,12 +25,17 @@ export class CodeExampleComponent implements OnInit {
|
|||
code: string;
|
||||
language: string;
|
||||
linenums: boolean | number;
|
||||
path: string;
|
||||
region: string;
|
||||
title: string;
|
||||
|
||||
constructor(private elementRef: ElementRef) {
|
||||
const element = this.elementRef.nativeElement;
|
||||
|
||||
this.language = element.getAttribute('language') || '';
|
||||
this.linenums = element.getAttribute('linenums');
|
||||
this.path = element.getAttribute('path') || '';
|
||||
this.region = element.getAttribute('region') || '';
|
||||
this.title = element.getAttribute('title') || '';
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
import { Component, ElementRef, OnInit } from '@angular/core';
|
||||
|
||||
export interface TabInfo {
|
||||
title: string;
|
||||
language: string;
|
||||
class: string;
|
||||
code: string;
|
||||
language: string;
|
||||
linenums: any;
|
||||
path: string;
|
||||
region: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,14 +21,15 @@ export interface TabInfo {
|
|||
@Component({
|
||||
selector: 'code-tabs',
|
||||
template: `
|
||||
<md-tab-group class="code-tab-group">
|
||||
<md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||
<ng-template md-tab-label>
|
||||
<span class="{{tab.class}}">{{ tab.title }}</span>
|
||||
</ng-template>
|
||||
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums" class="{{ tab.class }}"></aio-code>
|
||||
</md-tab>
|
||||
</md-tab-group>
|
||||
<md-tab-group class="code-tab-group">
|
||||
<md-tab style="overflow-y: hidden;" *ngFor="let tab of tabs">
|
||||
<ng-template md-tab-label>
|
||||
<span class="{{tab.class}}">{{ tab.title }}</span>
|
||||
</ng-template>
|
||||
<aio-code [code]="tab.code" [language]="tab.language" [linenums]="tab.linenums"
|
||||
[path]="tab.path" [region]="tab.region" class="{{ tab.class }}"></aio-code>
|
||||
</md-tab>
|
||||
</md-tab-group>
|
||||
`
|
||||
})
|
||||
export class CodeTabsComponent implements OnInit {
|
||||
|
@ -59,6 +64,8 @@ export class CodeTabsComponent implements OnInit {
|
|||
class: codeExample.getAttribute('class'),
|
||||
language: codeExample.getAttribute('language'),
|
||||
linenums: this.getLinenums(codeExample),
|
||||
path: codeExample.getAttribute('path') || '',
|
||||
region: codeExample.getAttribute('region') || '',
|
||||
title: codeExample.getAttribute('title')
|
||||
};
|
||||
this.tabs.push(tab);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { PrettyPrinter } from './pretty-printer.service';
|
|||
|
||||
const oneLineCode = 'const foo = "bar";';
|
||||
|
||||
const multiLineCode = `
|
||||
const smallMultiLineCode = `
|
||||
<hero-details>
|
||||
<h2>Bah Dah Bing</h2>
|
||||
<hero-team>
|
||||
|
@ -20,6 +20,8 @@ const multiLineCode = `
|
|||
</hero-team>
|
||||
</hero-details>`;
|
||||
|
||||
const bigMultiLineCode = smallMultiLineCode + smallMultiLineCode + smallMultiLineCode;
|
||||
|
||||
describe('CodeComponent', () => {
|
||||
let codeComponentDe: DebugElement;
|
||||
let codeComponent: CodeComponent;
|
||||
|
@ -75,38 +77,38 @@ describe('CodeComponent', () => {
|
|||
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', () => {
|
||||
// `<li>`s are a tell-tale for line numbers
|
||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
||||
expect(lis.length).toBe(0, 'should be no linenums');
|
||||
expect(hasLineNumbers()).toBe(false);
|
||||
});
|
||||
|
||||
it('should add line numbers to one-line code sample when linenums set true', () => {
|
||||
hostComponent.linenums = 'true';
|
||||
fixture.detectChanges();
|
||||
|
||||
// `<li>`s are a tell-tale for line numbers
|
||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
||||
expect(lis.length).toBe(1, 'has linenums');
|
||||
expect(hasLineNumbers()).toBe(true);
|
||||
});
|
||||
|
||||
it('should format multi-line code with linenums by default', () => {
|
||||
hostComponent.code = multiLineCode;
|
||||
it('should format a small multi-line code without linenums by default', () => {
|
||||
hostComponent.code = smallMultiLineCode;
|
||||
fixture.detectChanges();
|
||||
|
||||
// `<li>`s are a tell-tale for line numbers
|
||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
||||
expect(lis.length).toBeGreaterThan(0, 'has linenums');
|
||||
expect(hasLineNumbers()).toBe(false);
|
||||
});
|
||||
|
||||
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.code = multiLineCode;
|
||||
hostComponent.code = bigMultiLineCode;
|
||||
fixture.detectChanges();
|
||||
|
||||
// `<li>`s are a tell-tale for line numbers
|
||||
const lis = codeComponentDe.nativeElement.querySelectorAll('li');
|
||||
expect(lis.length).toBe(0, 'should be no linenums');
|
||||
expect(hasLineNumbers()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -121,7 +123,7 @@ describe('CodeComponent', () => {
|
|||
|
||||
it('should trim whitespace from the code before rendering', () => {
|
||||
hostComponent.linenums = false;
|
||||
hostComponent.code = '\n\n\n' + multiLineCode + '\n\n\n';
|
||||
hostComponent.code = '\n\n\n' + smallMultiLineCode + '\n\n\n';
|
||||
fixture.detectChanges();
|
||||
const codeContent = codeComponentDe.nativeElement.querySelector('code').innerText;
|
||||
expect(codeContent).toEqual(codeContent.trim());
|
||||
|
@ -137,18 +139,42 @@ describe('CodeComponent', () => {
|
|||
});
|
||||
|
||||
describe('error message', () => {
|
||||
it('should display error message when there is no code (after trimming)', () => {
|
||||
hostComponent.code = ' \n ';
|
||||
fixture.detectChanges();
|
||||
const missing = codeComponentDe.nativeElement.querySelector('.code-missing') as HTMLElement;
|
||||
expect(missing).not.toBeNull('should have element with "code-missing" class');
|
||||
expect(missing.innerText).toContain('missing', 'error message');
|
||||
});
|
||||
|
||||
function getErrorMessage() {
|
||||
const missing: HTMLElement = codeComponentDe.nativeElement.querySelector('.code-missing');
|
||||
return missing ? missing.innerText : null;
|
||||
}
|
||||
|
||||
it('should not display "code-missing" class when there is some code', () => {
|
||||
fixture.detectChanges();
|
||||
const missing = codeComponentDe.nativeElement.querySelector('.code-missing');
|
||||
expect(missing).toBeNull('should not have element with "code-missing" class');
|
||||
expect(getErrorMessage()).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({
|
||||
selector: 'aio-host-comp',
|
||||
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 {
|
||||
code = oneLineCode;
|
||||
language: string;
|
||||
linenums: boolean | number | string;
|
||||
path: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
class TestLogger {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { MdSnackBar } from '@angular/material';
|
|||
|
||||
const originalLabel = 'Copy Code';
|
||||
const copiedLabel = 'Copied!';
|
||||
const defaultLineNumsCount = 10; // by default, show linenums over this number
|
||||
|
||||
/**
|
||||
* Formatted Code Block
|
||||
|
@ -17,14 +18,18 @@ const copiedLabel = 'Copied!';
|
|||
* 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({
|
||||
selector: 'aio-code',
|
||||
template: `
|
||||
|
||||
<pre class="prettyprint lang-{{language}}">
|
||||
<button *ngIf="code" class="material-icons copy-button" (click)="doCopy()">content_copy</button>
|
||||
<code class="animated fadeIn" #codeContainer></code>
|
||||
|
@ -33,6 +38,12 @@ const copiedLabel = 'Copied!';
|
|||
})
|
||||
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
|
||||
* (could be javascript, dart, typescript, etc)
|
||||
|
@ -50,10 +61,16 @@ export class CodeComponent implements OnChanges {
|
|||
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()
|
||||
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
|
||||
|
@ -70,7 +87,9 @@ export class CodeComponent implements OnChanges {
|
|||
this.code = this.code && leftAlign(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;
|
||||
}
|
||||
|
||||
|
@ -117,7 +136,7 @@ export class CodeComponent implements OnChanges {
|
|||
|
||||
// if no linenums, enable line numbers if more than one line
|
||||
return linenums == null || linenums === NaN ?
|
||||
(this.code.match(/\n/g) || []).length > 1 : linenums;
|
||||
(this.code.match(/\n/g) || []).length > defaultLineNumsCount : linenums;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@ module.exports = function renderExamples(getExampleRegion) {
|
|||
if (attrMap.path) {
|
||||
// We found a path attribute so look up the example and rebuild the HTML
|
||||
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, '"')}"`).join('');
|
||||
return `<${element}${attributes}>\n${exampleContent}\n</${element}>`;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ describe('renderExamples processor', () => {
|
|||
{ renderedContent: `<${CODE_TAG} path="test/url">Some code</${CODE_TAG}>`}
|
||||
];
|
||||
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`, () => {
|
||||
|
@ -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}>`}
|
||||
];
|
||||
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', () => {
|
||||
|
@ -61,7 +61,7 @@ describe('renderExamples processor', () => {
|
|||
{ renderedContent: `<${CODE_TAG} path="test/url" region="region-1">Some code</${CODE_TAG}>` }
|
||||
];
|
||||
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`, () => {
|
||||
|
@ -69,15 +69,16 @@ describe('renderExamples processor', () => {
|
|||
{ renderedContent: `<${CODE_TAG} path="test/url" region="">Some code</${CODE_TAG}>` }
|
||||
];
|
||||
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', () => {
|
||||
const docs = [
|
||||
{ renderedContent: `<${CODE_TAG} class="special" path="test/url" linenums="15" region="region-1" id="some-id">Some code</${CODE_TAG}>` }
|
||||
it('should pass along all attributes including path and region', () => {
|
||||
const openTag = `<${CODE_TAG} class="special" path="test/url" linenums="15" region="region-1" id="some-id">`;
|
||||
|
||||
const docs = [ { renderedContent: `${openTag}Some code</${CODE_TAG}>` }
|
||||
];
|
||||
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', () => {
|
||||
|
@ -85,7 +86,7 @@ describe('renderExamples processor', () => {
|
|||
{ renderedContent: `<${CODE_TAG} title='a "quoted" value' path="test/url"></${CODE_TAG}>`}
|
||||
];
|
||||
processor.$process(docs);
|
||||
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a "quoted" value">\nwhole file\n</${CODE_TAG}>`);
|
||||
expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a "quoted" value" path="test/url">\nwhole file\n</${CODE_TAG}>`);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue