From 837ed788f4d031efdb26fc642dc301537473d7f0 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Sun, 26 Mar 2017 13:32:29 -0700 Subject: [PATCH] feat(aio): add code-example and code-tabs * move embedded components to EmbeddedModule * add PrettyPrint service; load pretty print js dynamically * make code-example to syntax highlighting w/ `prettyPrintOne` * add code-tabs * Implement copy code button --- aio/e2e/app.e2e-spec.ts | 3 +- aio/src/app/app.module.ts | 12 +- .../embedded/code-example.component.spec.ts | 28 --- .../app/embedded/code-example.component.ts | 73 ------- .../code/code-example.component.spec.ts | 94 +++++++++ .../embedded/code/code-example.component.ts | 41 ++++ .../app/embedded/code/code-tabs.component.ts | 72 +++++++ .../app/embedded/code/code.component.spec.ts | 146 +++++++++++++ aio/src/app/embedded/code/code.component.ts | 114 +++++++++++ .../embedded/code/pretty-printer.service.ts | 63 ++++++ aio/src/app/embedded/embedded.module.ts | 46 +++++ aio/src/app/embedded/index.ts | 13 -- .../doc-viewer/doc-viewer.component.spec.ts | 5 +- .../layout/doc-viewer/doc-viewer.component.ts | 2 +- aio/src/app/shared/copier.service.ts | 66 ++++++ aio/src/assets/js/prettify.js | 46 +++++ aio/src/styles/2-modules/_code.scss | 192 ++++++++++++++++-- aio/src/styles/_constants.scss | 3 +- .../templates/example-region.template.html | 2 +- 19 files changed, 877 insertions(+), 144 deletions(-) delete mode 100644 aio/src/app/embedded/code-example.component.spec.ts delete mode 100644 aio/src/app/embedded/code-example.component.ts create mode 100644 aio/src/app/embedded/code/code-example.component.spec.ts create mode 100644 aio/src/app/embedded/code/code-example.component.ts create mode 100644 aio/src/app/embedded/code/code-tabs.component.ts create mode 100644 aio/src/app/embedded/code/code.component.spec.ts create mode 100644 aio/src/app/embedded/code/code.component.ts create mode 100644 aio/src/app/embedded/code/pretty-printer.service.ts create mode 100644 aio/src/app/embedded/embedded.module.ts delete mode 100644 aio/src/app/embedded/index.ts create mode 100644 aio/src/app/shared/copier.service.ts create mode 100755 aio/src/assets/js/prettify.js diff --git a/aio/e2e/app.e2e-spec.ts b/aio/e2e/app.e2e-spec.ts index 6a17b01ab1..1c72e2f274 100644 --- a/aio/e2e/app.e2e-spec.ts +++ b/aio/e2e/app.e2e-spec.ts @@ -34,8 +34,7 @@ describe('site App', function() { it('should render `{@example}` dgeni tags as `` elements with HTML escaped content', () => { page.navigateTo('guide/component-styles'); const codeExample = element.all(by.css('code-example')).first(); - expect(page.getInnerHtml(codeExample)) - .toContain('@Component({\n selector: \'hero-app\',\n template: `\n <h1>Tour of Heroes</h1>'); + expect(page.getInnerHtml(codeExample)).toContain('<h1>Tour of Heroes</h1>'); }); describe('api-docs', () => { diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 13781a4e86..45972fa9f0 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -1,6 +1,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpModule } from '@angular/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; @@ -9,6 +10,7 @@ import { MdButtonModule} from '@angular/material/button'; import { MdIconModule} from '@angular/material/icon'; import { MdInputModule } from '@angular/material/input'; import { MdSidenavModule } from '@angular/material/sidenav'; +import { MdTabsModule } from '@angular/material'; import { Platform } from '@angular/material/core'; // Temporary fix for MdSidenavModule issue: @@ -18,7 +20,7 @@ import 'rxjs/add/operator/first'; import { AppComponent } from 'app/app.component'; import { ApiService } from 'app/embedded/api/api.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; -import { embeddedComponents, EmbeddedComponents } from 'app/embedded'; +import { EmbeddedModule } from 'app/embedded/embedded.module'; import { GaService } from 'app/shared/ga.service'; import { Logger } from 'app/shared/logger.service'; import { LocationService } from 'app/shared/location.service'; @@ -35,16 +37,18 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service'; @NgModule({ imports: [ BrowserModule, + EmbeddedModule, HttpModule, + BrowserAnimationsModule, MdButtonModule, MdIconModule, MdInputModule, MdToolbarModule, - MdSidenavModule + MdSidenavModule, + MdTabsModule ], declarations: [ AppComponent, - embeddedComponents, DocViewerComponent, TopMenuComponent, NavMenuComponent, @@ -54,7 +58,6 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service'; ], providers: [ ApiService, - EmbeddedComponents, GaService, Logger, Location, @@ -66,7 +69,6 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service'; Platform, AutoScrollService, ], - entryComponents: [ embeddedComponents ], bootstrap: [AppComponent] }) export class AppModule { diff --git a/aio/src/app/embedded/code-example.component.spec.ts b/aio/src/app/embedded/code-example.component.spec.ts deleted file mode 100644 index 4c1d154a32..0000000000 --- a/aio/src/app/embedded/code-example.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; - -import { CodeExampleComponent } from './code-example.component'; - -describe('CodeExampleComponent', () => { - let component: CodeExampleComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ CodeExampleComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CodeExampleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/aio/src/app/embedded/code-example.component.ts b/aio/src/app/embedded/code-example.component.ts deleted file mode 100644 index 7b79bc736a..0000000000 --- a/aio/src/app/embedded/code-example.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable component-selector */ - -import { Component, OnInit, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; - -// TODO(i): add clipboard copy functionality - -/** - * Angular.io Code Example - * - * Pretty renders a code block, primarily used in the docs and API reference. Can be used within an Angular app, or - * independently, provided that it is dynamically generated by the component resolver. - * - * Usage: - * - * console.log('Hello World') - * - */ -@Component({ - selector: 'code-example', - template: '
' -}) -export class CodeExampleComponent implements OnInit, AfterViewInit { - - @ViewChild('codeContainer') codeContainerRef: ElementRef; - - language: string; // could be javascript, dart, typescript - // TODO(i): escape doesn't seem to be currently supported in the original code - escape: string; // could be 'html' - format: string; // some css class - showcase: string; // a string with the value 'true' - animated = false; - - // TODO(i): could we use @HostBinding instead or does the CSS have to be scoped to
 and 
-  classes: string;
-  animatedClasses: string;
-
-
-  constructor(private elementRef: ElementRef) {
-    // TODO(i): @Input should be supported for host elements and should just do a one off initialization of properties
-    //          from the host element => talk to Tobias
-    ['language', 'escape', 'format', 'showcase', 'animated'].forEach(inputName => {
-      if (!this[inputName]) {
-        this[inputName] = this.elementRef.nativeElement.getAttribute(inputName);
-      }
-    });
-  }
-
-
-  ngOnInit() {
-    const showcaseClass = this.showcase === 'true' ? ' is-showcase' : '';
-    this.classes = `
-      prettyprint
-      ${this.format ? this.format : ''}
-      ${this.language ? 'lang-' + this.language : '' }
-      ${showcaseClass ? showcaseClass : ''}
-    `.trim();
-
-    this.animatedClasses = `${this.animated ? 'animated fadeIn' : ''}`;
-
-    // Security: the codeExampleContent is the original innerHTML of the host element provided by
-    //           docs authors and as such its considered to be safe for innerHTML purposes
-    this.codeContainerRef.nativeElement.innerHTML = this.elementRef.nativeElement.codeExampleContent;
-  }
-
-
-  ngAfterViewInit() {
-    // TODO(i): import prettify.js from this file so that we don't need to preload it via index.html
-    // whenever a code example is used, use syntax highlighting.
-    // if(prettyPrint) {
-    //   prettyPrint();
-    // }
-  }
-}
diff --git a/aio/src/app/embedded/code/code-example.component.spec.ts b/aio/src/app/embedded/code/code-example.component.spec.ts
new file mode 100644
index 0000000000..987258b884
--- /dev/null
+++ b/aio/src/app/embedded/code/code-example.component.spec.ts
@@ -0,0 +1,94 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Component, DebugElement, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
+
+import { CodeExampleComponent } from './code-example.component';
+
+describe('CodeExampleComponent', () => {
+  let hostComponent: HostComponent;
+  let codeComponent: TestCodeComponent;
+  let codeExampleDe: DebugElement;
+  let codeExampleComponent: CodeExampleComponent;
+  let fixture: ComponentFixture;
+
+  const oneLineCode = `const foo = "bar";`;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [ CodeExampleComponent, HostComponent, TestCodeComponent ],
+    });
+  });
+
+  function createComponent(codeExampleContent = '') {
+    fixture = TestBed.createComponent(HostComponent);
+    hostComponent = fixture.componentInstance;
+    codeExampleDe = fixture.debugElement.children[0];
+    codeExampleComponent = codeExampleDe.componentInstance;
+    codeComponent = codeExampleDe.query(By.directive(TestCodeComponent)).componentInstance;
+
+    // Copy the CodeExample's innerHTML (content)
+    // into the `codeExampleContent` property as the DocViewer does
+    codeExampleDe.nativeElement.codeExampleContent = codeExampleContent;
+
+    fixture.detectChanges();
+  }
+
+  it('should create CodeExampleComponent', () => {
+    createComponent();
+    expect(codeExampleComponent).toBeTruthy('CodeExampleComponent');
+  });
+
+  it('should pass content to CodeComponent ()', () => {
+    createComponent(oneLineCode);
+    expect(codeComponent.code).toBe(oneLineCode);
+  });
+
+  it('should pass language to CodeComponent', () => {
+    TestBed.overrideComponent(HostComponent, {
+      set: {template: ''}});
+    createComponent(oneLineCode);
+    expect(codeComponent.language).toBe('html');
+  });
+
+  it('should pass linenums to CodeComponent', () => {
+    TestBed.overrideComponent(HostComponent, {
+      set: {template: ''}});
+    createComponent(oneLineCode);
+    expect(codeComponent.linenums).toBe('true');
+  });
+
+  it('should add title (header) when set `title` attribute', () => {
+    TestBed.overrideComponent(HostComponent, {
+      set: {template: ''}});
+    createComponent(oneLineCode);
+    const actual = codeExampleDe.query(By.css('header')).nativeElement.innerText;
+    expect(actual).toBe('Great Example');
+  });
+});
+
+//// Test helpers ////
+// tslint:disable:member-ordering
+@Component({
+  selector: 'aio-code',
+  template: `
+  
lang: {{language}}
+
linenums: {{linenums}}
+ code:
{{someCode}}
+ ` +}) +class TestCodeComponent { + @Input() code = ''; + @Input() language: string; + @Input() linenums: boolean | number; + + get someCode() { + return this.code && this.code.length > 30 ? this.code.substr(0, 30) + '...' : this.code; + } +} + +@Component({ + selector: 'aio-host-comp', + template: `` +}) +class HostComponent { } diff --git a/aio/src/app/embedded/code/code-example.component.ts b/aio/src/app/embedded/code/code-example.component.ts new file mode 100644 index 0000000000..8017cc8b89 --- /dev/null +++ b/aio/src/app/embedded/code/code-example.component.ts @@ -0,0 +1,41 @@ +/* tslint:disable component-selector */ +import { Component, ElementRef, OnInit } from '@angular/core'; + +/** + * An embeddable code block that displays nicely formatted code. + * Example usage: + * + * ``` + * + * // a code block + * console.log('do stuff'); + * + * ``` + */ +@Component({ + selector: 'code-example', + template: ` +
{{title}}
+ + ` +}) +export class CodeExampleComponent implements OnInit { // implements AfterViewInit { + + code: string; + language: string; + linenums: boolean | number; + title: string; + + constructor(private elementRef: ElementRef) { + const element = this.elementRef.nativeElement; + this.language = element.getAttribute('language') || ''; + this.linenums = element.getAttribute('linenums'); + this.title = element.getAttribute('title') || ''; + } + + ngOnInit() { + // The `codeExampleContent` property is set by the DocViewer when it builds this component. + // It is the original innerHTML of the host element. + this.code = this.elementRef.nativeElement.codeExampleContent; + } +} diff --git a/aio/src/app/embedded/code/code-tabs.component.ts b/aio/src/app/embedded/code/code-tabs.component.ts new file mode 100644 index 0000000000..374e93ffdf --- /dev/null +++ b/aio/src/app/embedded/code/code-tabs.component.ts @@ -0,0 +1,72 @@ +/* tslint:disable component-selector */ +import { Component, ElementRef, OnInit } from '@angular/core'; + +export interface TabInfo { + title: string; + language: string; + code: string; +} + +/** + * An embedded component used to generate tabbed code panes inside docs + * + * The innerHTML of the `` component should contain `` elements. + * Each `` has the same interface as the embedded `` component. + * The optional `linenums` attribute is the default `linenums` for each code pane. + */ +@Component({ + selector: 'code-tabs', + template: ` + + + + + + + ` +}) +export class CodeTabsComponent implements OnInit { + tabs: TabInfo[]; + linenumsDefault: string; + + constructor(private elementRef: ElementRef) { } + + ngOnInit() { + const element = this.elementRef.nativeElement; + this.linenumsDefault = this.getLinenums(element); + + // The `codeTabsContent` property is set by the DocViewer when it builds this component. + // It is the original innerHTML of the host element. + const content = element.codeTabsContent; + this.processContent(content); + } + + processContent(content: string) { + // We add it to an element so that we can easily parse the HTML + const element = document.createElement('div'); + // **Security:** `codeTabsContent` is provided by docs authors and as such its considered to + // be safe for innerHTML purposes. + element.innerHTML = content; + + this.tabs = []; + const codeExamples = element.querySelectorAll('code-pane'); + for (let i = 0; i < codeExamples.length; i++) { + const codeExample = codeExamples.item(i); + const tab = { + code: codeExample.innerHTML, + class: codeExample.getAttribute('class'), + language: codeExample.getAttribute('language'), + linenums: this.getLinenums(codeExample), + title: codeExample.getAttribute('title') + }; + this.tabs.push(tab); + } + } + + getLinenums(element: Element) { + const linenums = element.getAttribute('linenums'); + return linenums == null ? this.linenumsDefault : linenums; + } +} diff --git a/aio/src/app/embedded/code/code.component.spec.ts b/aio/src/app/embedded/code/code.component.spec.ts new file mode 100644 index 0000000000..145c583280 --- /dev/null +++ b/aio/src/app/embedded/code/code.component.spec.ts @@ -0,0 +1,146 @@ +/* tslint:disable:no-unused-variable */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { Component, DebugElement } from '@angular/core'; + +import { CodeComponent } from './code.component'; +import { CopierService } from 'app/shared//copier.service'; +import { Logger } from 'app/shared/logger.service'; +import { PrettyPrinter } from './pretty-printer.service'; + +const oneLineCode = 'const foo = "bar";'; + +const multiLineCode = ` +<hero-details> + <h2>Bah Dah Bing</h2> + <hero-team> + <h3>NYC Team</h3> + </hero-team> +</hero-details>`; + +describe('CodeComponent', () => { + let codeComponentDe: DebugElement; + let codeComponent: CodeComponent; + let hostComponent: HostComponent; + let fixture: ComponentFixture; + + + // WARNING: Chance of cross-test pollution + // CodeComponent injects PrettyPrintService + // Once PrettyPrintService runs once _anywhere_, its ctor loads `prettify.js` + // which sets `window['prettyPrintOne']` + // That global survives these tests unless + // we take strict measures to wipe it out in the `afterAll` + // and make sure THAT runs after the tests by making component creation async + afterAll(() => { + delete window['prettyPrint']; + delete window['prettyPrintOne']; + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CodeComponent, HostComponent ], + providers: [ + PrettyPrinter, + {provide: CopierService, useClass: TestCopierService }, + {provide: Logger, useClass: TestLogger } + ] + }) + .compileComponents(); + })); + + // Must be async because + // CodeComponent creates PrettyPrintService which async loads `prettify.js`. + // If not async, `afterAll` finishes before tests do! + beforeEach(async(() => { + fixture = TestBed.createComponent(HostComponent); + hostComponent = fixture.componentInstance; + codeComponentDe = fixture.debugElement.children[0]; + codeComponent = codeComponentDe.componentInstance; + fixture.detectChanges(); + })); + + it('should create CodeComponent', () => { + expect(codeComponentDe.name).toBe('aio-code', 'selector'); + expect(codeComponent).toBeTruthy('CodeComponent'); + }); + + it('should format a one-line code sample', () => { + // 'pln' spans are a tell-tale for syntax highlighing + const spans = codeComponentDe.nativeElement.querySelectorAll('span.pln'); + expect(spans.length).toBeGreaterThan(0, 'formatted spans'); + }); + + 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'); + }); + + 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'); + }); + + it('should format multi-line code with linenums by default', () => { + hostComponent.code = multiLineCode; + fixture.detectChanges(); + + // `
  • `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', () => { + hostComponent.linenums = false; + hostComponent.code = multiLineCode; + 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'); + }); + + it('should call copier service when copy button clicked', () => { + const copierService: TestCopierService = codeComponentDe.injector.get(CopierService) ; + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(copierService.copyText.calls.count()).toBe(0, 'before click'); + button.click(); + expect(copierService.copyText.calls.count()).toBe(1, 'after click'); + }); + + it('should copy code text when copy button clicked', () => { + const copierService: TestCopierService = codeComponentDe.injector.get(CopierService) ; + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + expect(copierService.copyText.calls.argsFor(0)[0]).toEqual(oneLineCode, 'after click'); + }); + +}); + +//// Test helpers //// +// tslint:disable:member-ordering +@Component({ + selector: 'aio-host-comp', + template: ` + + ` +}) +class HostComponent { + code = oneLineCode; + language: string; + linenums: boolean | number | string; +} + +class TestCopierService { + copyText = jasmine.createSpy('copyText'); +} + +class TestLogger { + log = jasmine.createSpy('log'); + error = jasmine.createSpy('error'); +} diff --git a/aio/src/app/embedded/code/code.component.ts b/aio/src/app/embedded/code/code.component.ts new file mode 100644 index 0000000000..21cc48b6c2 --- /dev/null +++ b/aio/src/app/embedded/code/code.component.ts @@ -0,0 +1,114 @@ +import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@angular/core'; +import { Logger } from 'app/shared/logger.service'; +import { PrettyPrinter } from './pretty-printer.service'; +import { CopierService } from 'app/shared/copier.service'; + +const originalLabel = 'Copy Code'; +const copiedLabel = 'Copied!'; + +/** + * Formatted Code Block + * + * Pretty renders a code block, used in the docs and API reference by the code-example and + * code-tabs embedded components. + * It includes a "copy" button that will send the content to the clipboard when clicked + * + * Example usage: + * + * ``` + * + * ``` + * + */ +@Component({ + selector: 'aio-code', + template: ` + +
    +      
    +    
    + ` +}) +export class CodeComponent implements OnChanges { + + /** + * The language of the code to render + * (could be javascript, dart, typescript, etc) + */ + @Input() + language: string; + + /** + * Whether to display line numbers: + * - false: don't display + * - true: do display + * - number: do display but start at the given number + */ + @Input() + linenums: boolean | number | string; + + /** + * The code to be formatted, this should already be HTML encoded + */ + @Input() + code: string; + + /** + * The label to show on the copy button + */ + buttonLabel = originalLabel; + + /** + * The element in the template that will display the formatted code + */ + @ViewChild('codeContainer') codeContainer: ElementRef; + + constructor( + private pretty: PrettyPrinter, + private copier: CopierService, + private logger: Logger) {} + + ngOnChanges() { + if (!this.code) { return; } + + const linenums = this.getLinenums(); + + this.setCodeHtml(this.code); // start with unformatted code + this.pretty.formatCode(this.code, this.language, linenums).subscribe( + formattedCode => this.setCodeHtml(formattedCode), + err => { /* ignore failure to format */ } + ); + } + + private setCodeHtml(formattedCode: string) { + // **Security:** `codeExampleContent` is provided by docs authors and as such its considered to + // be safe for innerHTML purposes. + this.codeContainer.nativeElement.innerHTML = formattedCode; + } + + doCopy() { + // We take the innerText because we don't want it to be HTML encoded + const code = this.codeContainer.nativeElement.innerText; + if (this.copier.copyText(code)) { + this.logger.log('Copied code to clipboard:', code); + // change the button label (for one second) + this.buttonLabel = copiedLabel; + setTimeout(() => this.buttonLabel = originalLabel, 1000); + } else { + this.logger.error('ERROR copying code to clipboard:', code); + } + } + + getLinenums() { + const linenums = + typeof this.linenums === 'boolean' ? this.linenums : + this.linenums === 'true' ? true : + this.linenums === 'false' ? false : + typeof this.linenums === 'string' ? parseInt(this.linenums, 10) : + this.linenums; + + // if no linenums, enable line numbers if more than one line + return linenums == null || linenums === NaN ? + (this.code.match(/\n/g) || []).length > 1 : linenums; + } +} diff --git a/aio/src/app/embedded/code/pretty-printer.service.ts b/aio/src/app/embedded/code/pretty-printer.service.ts new file mode 100644 index 0000000000..6fdd982908 --- /dev/null +++ b/aio/src/app/embedded/code/pretty-printer.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { fromPromise } from 'rxjs/observable/fromPromise'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/first'; + +import { Logger } from 'app/shared/logger.service'; + +declare const System; + +type PrettyPrintOne = (code: string, language?: string, linenums?: number | boolean) => string; + +/** + * Wrapper around the prettify.js library + */ +@Injectable() +export class PrettyPrinter { + + private prettyPrintOne: Observable; + + constructor(private logger: Logger) { + this.prettyPrintOne = fromPromise(this.getPrettyPrintOne()).share(); + } + + private getPrettyPrintOne(): Promise { + const ppo = window['prettyPrintOne']; + return ppo ? Promise.resolve(ppo) : + // prettify.js is not in window global; load it with webpack loader + System.import('assets/js/prettify.js') + .then( + () => window['prettyPrintOne'], + err => { + const msg = 'Cannot get prettify.js from server'; + this.logger.error(msg, err); + // return a pretty print fn that always fails. + return () => { throw new Error(msg); }; + }); + } + + /** + * Format code snippet as HTML + * @param {string} code - the code snippet to format; should already be HTML encoded + * @param {string} [language] - The language of the code to render (could be javascript, html, typescript, etc) + * @param {string|number} [linenums] - Whether to display line numbers: + * - false: don't display + * - true: do display + * - number: do display but start at the given number + * @returns Observable - Observable of formatted code + */ + formatCode(code: string, language?: string, linenums?: number | boolean) { + return this.prettyPrintOne.map(ppo => { + try { + return ppo(code, language, linenums); + } catch (err) { + const msg = `Could not format code that begins '${code.substr(0, 50)}...'.`; + console.error(msg, err); + throw new Error(msg); + } + }) + .first(); // complete immediately + } +} diff --git a/aio/src/app/embedded/embedded.module.ts b/aio/src/app/embedded/embedded.module.ts new file mode 100644 index 0000000000..588d6bf089 --- /dev/null +++ b/aio/src/app/embedded/embedded.module.ts @@ -0,0 +1,46 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { PrettyPrinter } from './code/pretty-printer.service'; +import { CopierService } from 'app/shared/copier.service'; + + +// Any components that we want to use inside embedded components must be declared or imported here +// It is not enough just to import them inside the AppModule + +// Reusable components (used inside embedded components) +import { MdTabsModule } from '@angular/material'; +import { CodeComponent } from './code/code.component'; + +// Embedded Components +import { ApiListComponent } from './api/api-list.component'; +import { CodeExampleComponent } from './code/code-example.component'; +import { CodeTabsComponent } from './code/code-tabs.component'; +import { DocTitleComponent } from './doc-title.component'; + +/** Components that can be embedded in docs + * such as CodeExampleComponent, LiveExampleComponent,... + */ +export const embeddedComponents: any[] = [ + ApiListComponent, CodeExampleComponent, DocTitleComponent, CodeTabsComponent +]; + +/** Injectable class w/ property returning components that can be embedded in docs */ +export class EmbeddedComponents { + components = embeddedComponents; +} + +@NgModule({ + imports: [ CommonModule, MdTabsModule ], + declarations: [ + embeddedComponents, + CodeComponent + ], + providers: [ + EmbeddedComponents, + PrettyPrinter, + CopierService + ], + entryComponents: [ embeddedComponents ] +}) +export class EmbeddedModule { } diff --git a/aio/src/app/embedded/index.ts b/aio/src/app/embedded/index.ts deleted file mode 100644 index be04f07a42..0000000000 --- a/aio/src/app/embedded/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiListComponent } from './api/api-list.component'; -import { CodeExampleComponent } from './code-example.component'; -import { DocTitleComponent } from './doc-title.component'; - -/** Components that can be embedded in docs such as CodeExampleComponent, LiveExampleComponent,... */ -export const embeddedComponents: any[] = [ - ApiListComponent, CodeExampleComponent, DocTitleComponent -]; - -/** Injectable class w/ property returning components that can be embedded in docs */ -export class EmbeddedComponents { - components = embeddedComponents; -} diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts index a40dee42aa..35ee26608c 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFactoryResolver, ElementRef, Injector, NgModule, OnInit, ViewC import { By } from '@angular/platform-browser'; import { DocViewerComponent } from './doc-viewer.component'; import { DocumentContents } from 'app/documents/document.service'; -import { embeddedComponents, EmbeddedComponents } from 'app/embedded'; +import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module'; /// Embedded Test Components /// @@ -65,9 +65,10 @@ class BazComponent implements OnInit { } ///// Test Module ////// -const embeddedTestComponents = [FooComponent, BarComponent, BazComponent, ...embeddedComponents]; +const embeddedTestComponents = [FooComponent, BarComponent, BazComponent]; @NgModule({ + imports: [ EmbeddedModule ], entryComponents: embeddedTestComponents }) class TestModule { } diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts index a81c407bc9..e9cec3e6cb 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts @@ -4,7 +4,7 @@ import { Output, ViewEncapsulation } from '@angular/core'; -import { EmbeddedComponents } from 'app/embedded'; +import { EmbeddedComponents } from 'app/embedded/embedded.module'; import { DocumentContents } from 'app/documents/document.service'; interface EmbeddedComponentFactory { diff --git a/aio/src/app/shared/copier.service.ts b/aio/src/app/shared/copier.service.ts new file mode 100644 index 0000000000..301cffe465 --- /dev/null +++ b/aio/src/app/shared/copier.service.ts @@ -0,0 +1,66 @@ +/** + * This class is based on the code in the following projects: + * + * - https://github.com/zenorocha/select + * - https://github.com/zenorocha/clipboard.js/ + * + * Both released under MIT license - © Zeno Rocha + */ + + +export class CopierService { + private fakeElem: HTMLTextAreaElement; + + /** + * Creates a fake textarea element, sets its value from `text` property, + * and makes a selection on it. + */ + createFake(text: string) { + const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; + + // Create a fake element to hold the contents to copy + this.fakeElem = document.createElement('textarea'); + + // Prevent zooming on iOS + this.fakeElem.style.fontSize = '12pt'; + + // Reset box model + this.fakeElem.style.border = '0'; + this.fakeElem.style.padding = '0'; + this.fakeElem.style.margin = '0'; + + // Move element out of screen horizontally + this.fakeElem.style.position = 'absolute'; + this.fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px'; + + // Move element to the same position vertically + const yPosition = window.pageYOffset || document.documentElement.scrollTop; + this.fakeElem.style.top = yPosition + 'px'; + + this.fakeElem.setAttribute('readonly', ''); + this.fakeElem.value = text; + + document.body.appendChild(this.fakeElem); + + this.fakeElem.select(); + this.fakeElem.setSelectionRange(0, this.fakeElem.value.length); + } + + removeFake() { + if (this.fakeElem) { + document.body.removeChild(this.fakeElem); + this.fakeElem = null; + } + } + + copyText(text: string) { + try { + this.createFake(text); + return document.execCommand('copy'); + } catch (err) { + return false; + } finally { + this.removeFake(); + } + } +} diff --git a/aio/src/assets/js/prettify.js b/aio/src/assets/js/prettify.js new file mode 100755 index 0000000000..3b74b5bdaa --- /dev/null +++ b/aio/src/assets/js/prettify.js @@ -0,0 +1,46 @@ +!function(){/* + + Copyright (C) 2006 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function T(a){function d(e){var b=e.charCodeAt(0);if(92!==b)return b;var a=e.charAt(1);return(b=w[a])?b:"0"<=a&&"7">=a?parseInt(e.substring(1),8):"u"===a||"x"===a?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\s\S]|-|[^-\\]/g);e= +[];var a="^"===b[0],c=["["];a&&c.push("^");for(var a=a?1:0,g=b.length;ak||122k||90k||122h[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(f(h[1])));c.push("]");return c.join("")}function v(e){for(var a=e.source.match(/(?:\[(?:[^\x5C\x5D]|\\[\s\S])*\]|\\u[A-Fa-f0-9]{4}|\\x[A-Fa-f0-9]{2}|\\[0-9]+|\\[^ux0-9]|\(\?[:!=]|[\(\)\^]|[^\x5B\x5C\(\)\^]+)/g),c=a.length,d=[],g=0,h=0;g/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null]));if(b=a.regexLiterals){var v=(b=1|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+ +("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+v+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+v+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&f.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&f.push(["kwd",new RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i, +null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(b),null]);return G(d,f)}function L(a,d,f){function b(a){var c=a.nodeType;if(1==c&&!A.test(a.className))if("br"===a.nodeName)v(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((3==c||4==c)&&f){var d=a.nodeValue,q=d.match(n);q&&(c=d.substring(0,q.index),a.nodeValue=c,(d=d.substring(q.index+q[0].length))&& +a.parentNode.insertBefore(l.createTextNode(d),a.nextSibling),v(a),c||a.parentNode.removeChild(a))}}function v(a){function b(a,c){var d=c?a.cloneNode(!1):a,k=a.parentNode;if(k){var k=b(k,1),e=a.nextSibling;k.appendChild(d);for(var f=e;f;f=e)e=f.nextSibling,k.appendChild(f)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=b(a.nextSibling,0);for(var d;(d=a.parentNode)&&1===d.nodeType;)a=d;c.push(a)}for(var A=/(?:^|\s)nocode(?:\s|$)/,n=/\r\n?|\n/,l=a.ownerDocument,m=l.createElement("li");a.firstChild;)m.appendChild(a.firstChild); +for(var c=[m],p=0;p=+v[1],d=/\n/g,A=a.a,n=A.length,f=0,l=a.c,m=l.length,b=0,c=a.g,p=c.length,w=0;c[p]=n;var r,e;for(e=r=0;e=h&&(b+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(x){E.console&&console.log(x&&x.stack||x)}}var E=window,C=["break,continue,do,else,for,if,return,while"], +F=[[C,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"], +O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"], +F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,function,get,implements,instanceof,interface,let,null,set,undefined,var,with,yield,Infinity,NaN"],Q=[C,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[C,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],C=[C,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"], +S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=y({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,C],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),I={};t(X,["default-code"]);t(G([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));t(G([["pln",/^[\s]+/,null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null, +"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(y({keywords:H, +hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(y({keywords:"null,true,false"}),["json"]);t(y({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(y({keywords:O,cStyleComments:!0}),["java"]);t(y({keywords:C,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(y({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(y({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", +hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(y({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(y({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(y({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0, +regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=E.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:y,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:E.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var b=document.createElement("div");b.innerHTML="
    "+a+"
    "; +b=b.firstChild;f&&L(b,f,!0);M({j:d,m:f,h:b,l:1,a:null,i:null,c:null,g:null});return b.innerHTML},prettyPrint:E.prettyPrint=function(a,d){function f(){for(var b=E.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;p