Skip to content

Commit

Permalink
feat(aio): code snippet source available & shown when code missing
Browse files Browse the repository at this point in the history
Tells reader (usually the author) what code file is missing
Also when no linenums specified, turn them on if num of lines > 10
  • Loading branch information
wardbell authored and juleskremer committed Aug 24, 2017
1 parent fc2a26f commit 37d51e6
Showing 7 changed files with 118 additions and 56 deletions.
2 changes: 2 additions & 0 deletions aio/src/app/embedded/code/code-example.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 7 additions & 1 deletion aio/src/app/embedded/code/code-example.component.ts
Original file line number Diff line number Diff line change
@@ -16,20 +16,26 @@ 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 {

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') || '';
}

27 changes: 17 additions & 10 deletions aio/src/app/embedded/code/code-tabs.component.ts
Original file line number Diff line number Diff line change
@@ -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);
83 changes: 56 additions & 27 deletions aio/src/app/embedded/code/code.component.spec.ts
Original file line number Diff line number Diff line change
@@ -12,14 +12,16 @@ import { PrettyPrinter } from './pretty-printer.service';

const oneLineCode = 'const foo = "bar";';

const multiLineCode = `
const smallMultiLineCode = `
&lt;hero-details&gt;
&lt;h2&gt;Bah Dah Bing&lt;/h2&gt;
&lt;hero-team&gt;
&lt;h3&gt;NYC Team&lt;/h3&gt;
&lt;/hero-team&gt;
&lt;/hero-details&gt;`;

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();
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 add line numbers to a big multi-line code by default', () => {
hostComponent.code = bigMultiLineCode;
fixture.detectChanges();
expect(hasLineNumbers()).toBe(true);
});

it('should not format multi-line code when linenums set false', () => {
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', () => {

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();
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();
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');
expect(getErrorMessage()).toContain('missing');
});

it('should not display "code-missing" class when there is some code', () => {
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();
const missing = codeComponentDe.nativeElement.querySelector('.code-missing');
expect(missing).toBeNull('should not have element with "code-missing" class');
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 {
33 changes: 26 additions & 7 deletions aio/src/app/embedded/code/code.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

Original file line number Diff line number Diff line change
@@ -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, '&quot;')}"`).join('');
return `<${element}${attributes}>\n${exampleContent}\n</${element}>`;
}
Original file line number Diff line number Diff line change
@@ -45,47 +45,48 @@ 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`, () => {
const docs = [
{ 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', () => {
const docs = [
{ 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`, () => {
const docs = [
{ 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', () => {
const docs = [
{ renderedContent: `<${CODE_TAG} title='a "quoted" value' path="test/url"></${CODE_TAG}>`}
];
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}>`);
});
})
);

0 comments on commit 37d51e6

Please sign in to comment.