From 7b94f493b9ada2d900c09f6d769db4ec6c5394f8 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Thu, 27 Apr 2017 22:57:34 -0700 Subject: [PATCH] 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 --- .../code/code-example.component.spec.ts | 2 + .../embedded/code/code-example.component.ts | 8 +- .../app/embedded/code/code-tabs.component.ts | 27 ++++-- .../app/embedded/code/code.component.spec.ts | 91 ++++++++++++------- aio/src/app/embedded/code/code.component.ts | 33 +++++-- .../processors/render-examples.js | 2 - .../processors/render-examples.spec.js | 19 ++-- 7 files changed, 122 insertions(+), 60 deletions(-) diff --git a/aio/src/app/embedded/code/code-example.component.spec.ts b/aio/src/app/embedded/code/code-example.component.spec.ts index 987258b884..cd50303c7f 100644 --- a/aio/src/app/embedded/code/code-example.component.spec.ts +++ b/aio/src/app/embedded/code/code-example.component.spec.ts @@ -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; diff --git a/aio/src/app/embedded/code/code-example.component.ts b/aio/src/app/embedded/code/code-example.component.ts index b84f240d65..5ba2c0aeb8 100644 --- a/aio/src/app/embedded/code/code-example.component.ts +++ b/aio/src/app/embedded/code/code-example.component.ts @@ -16,7 +16,8 @@ import { Component, ElementRef, OnInit } from '@angular/core'; selector: 'code-example', template: `
{{title}}
- + ` }) 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') || ''; } diff --git a/aio/src/app/embedded/code/code-tabs.component.ts b/aio/src/app/embedded/code/code-tabs.component.ts index a9300ea0be..a7f0556cdd 100644 --- a/aio/src/app/embedded/code/code-tabs.component.ts +++ b/aio/src/app/embedded/code/code-tabs.component.ts @@ -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: ` - - - - {{ tab.title }} - - - - + + + + {{ tab.title }} + + + + ` }) 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); diff --git a/aio/src/app/embedded/code/code.component.spec.ts b/aio/src/app/embedded/code/code.component.spec.ts index 2ed1effdb2..fc1e1e0d9e 100644 --- a/aio/src/app/embedded/code/code.component.spec.ts +++ b/aio/src/app/embedded/code/code.component.spec.ts @@ -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 `
  • `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', () => { - // `
  • `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(); - - // `
  • `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(); - - // `
  • `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(); - - // `
  • `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: ` - + ` }) class HostComponent { code = oneLineCode; language: string; linenums: boolean | number | string; + path: string; + region: string; } class TestLogger { diff --git a/aio/src/app/embedded/code/code.component.ts b/aio/src/app/embedded/code/code.component.ts index 2f2bfe738f..49f62926ce 100644 --- a/aio/src/app/embedded/code/code.component.ts +++ b/aio/src/app/embedded/code/code.component.ts @@ -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: * * ``` - * + * + * * ``` - * */ @Component({ selector: 'aio-code', template: ` -
           
           
    @@ -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('

    The code sample is missing.

    '); + const src = this.path ? this.path + (this.region ? '#' + this.region : '') : ''; + const srcMsg = src ? ` for
    ${src}` : '.'; + this.setCodeHtml(`

    The code sample is missing${srcMsg}

    `); 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; } } diff --git a/aio/tools/transforms/examples-package/processors/render-examples.js b/aio/tools/transforms/examples-package/processors/render-examples.js index 35ecde9ac7..670dd0792a 100644 --- a/aio/tools/transforms/examples-package/processors/render-examples.js +++ b/aio/tools/transforms/examples-package/processors/render-examples.js @@ -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`; } diff --git a/aio/tools/transforms/examples-package/processors/render-examples.spec.js b/aio/tools/transforms/examples-package/processors/render-examples.spec.js index b02580a06f..c0e1f90a1f 100644 --- a/aio/tools/transforms/examples-package/processors/render-examples.spec.js +++ b/aio/tools/transforms/examples-package/processors/render-examples.spec.js @@ -45,7 +45,7 @@ describe('renderExamples processor', () => { { renderedContent: `<${CODE_TAG} path="test/url">Some code`} ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n`); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url">\nwhole file\n`); }); 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} path="test/url" region="region-1">Other code`} ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n<${CODE_TAG}>\nregion 1 contents\n`); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url">\nwhole file\n<${CODE_TAG} path="test/url" region="region-1">\nregion 1 contents\n`); }); 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` } ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nregion 1 contents\n`); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url" region="region-1">\nregion 1 contents\n`); }); 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` } ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n`); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} path="test/url" region="">\nwhole file\n`); }); - 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` } + 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` } ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} class="special" linenums="15" id="some-id">\nregion 1 contents\n`); + expect(docs[0].renderedContent).toEqual(`${openTag}\nregion 1 contents\n`); }); 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">`} ]; processor.$process(docs); - expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a "quoted" value">\nwhole file\n`); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a "quoted" value" path="test/url">\nwhole file\n`); }); }) );