2018-03-20 18:22:59 +02:00
|
|
|
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
|
2017-03-26 13:32:29 -07:00
|
|
|
import { Logger } from 'app/shared/logger.service';
|
|
|
|
import { PrettyPrinter } from './pretty-printer.service';
|
|
|
|
import { CopierService } from 'app/shared/copier.service';
|
2017-07-31 15:45:18 +03:00
|
|
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
2018-03-21 03:40:23 +02:00
|
|
|
import { tap } from 'rxjs/operators';
|
2017-03-26 13:32:29 -07:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/**
|
|
|
|
* If linenums is not set, this is the default maximum number of lines that
|
|
|
|
* an example can display without line numbers.
|
|
|
|
*/
|
|
|
|
const DEFAULT_LINE_NUMS_COUNT = 10;
|
2017-03-26 13:32:29 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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:
|
|
|
|
*
|
|
|
|
* ```
|
2017-04-27 22:57:34 -07:00
|
|
|
* <aio-code
|
|
|
|
* [language]="ts"
|
|
|
|
* [linenums]="true"
|
2017-08-14 20:58:56 -07:00
|
|
|
* [path]="router/src/app/app.module.ts"
|
|
|
|
* [region]="animations-module">
|
2017-04-27 22:57:34 -07:00
|
|
|
* </aio-code>
|
2017-03-26 13:32:29 -07:00
|
|
|
* ```
|
2018-02-28 12:05:59 -08:00
|
|
|
*
|
|
|
|
*
|
|
|
|
* Renders code provided through the `updateCode` method.
|
2017-03-26 13:32:29 -07:00
|
|
|
*/
|
|
|
|
@Component({
|
|
|
|
selector: 'aio-code',
|
|
|
|
template: `
|
|
|
|
<pre class="prettyprint lang-{{language}}">
|
2017-10-10 23:45:55 -07:00
|
|
|
<button *ngIf="!hideCopy" class="material-icons copy-button no-print"
|
2017-05-15 18:03:56 -07:00
|
|
|
title="Copy code snippet"
|
|
|
|
[attr.aria-label]="ariaLabel"
|
|
|
|
(click)="doCopy()">
|
|
|
|
<span aria-hidden="true">content_copy</span>
|
|
|
|
</button>
|
2017-03-26 13:32:29 -07:00
|
|
|
<code class="animated fadeIn" #codeContainer></code>
|
|
|
|
</pre>
|
|
|
|
`
|
|
|
|
})
|
|
|
|
export class CodeComponent implements OnChanges {
|
2017-05-15 18:03:56 -07:00
|
|
|
ariaLabel = '';
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** The code to be copied when clicking the copy button, this should not be HTML encoded */
|
2017-06-22 00:56:11 +03:00
|
|
|
private codeText: string;
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Code that should be formatted with current inputs and displayed in the view. */
|
|
|
|
set code(code: string) {
|
|
|
|
this._code = code;
|
2017-05-15 18:03:56 -07:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
if (!this._code || !this._code.trim()) {
|
|
|
|
this.showMissingCodeMessage();
|
|
|
|
} else {
|
|
|
|
this.formatDisplayedCode();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
get code(): string { return this._code; }
|
|
|
|
_code: string;
|
|
|
|
|
|
|
|
/** Whether the copy button should be shown. */
|
|
|
|
@Input() hideCopy: boolean;
|
|
|
|
|
|
|
|
/** Language to render the code (e.g. javascript, dart, typescript). */
|
|
|
|
@Input() language: string;
|
2017-03-26 13:32:29 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to display line numbers:
|
2018-02-28 12:05:59 -08:00
|
|
|
* - If false: hide
|
|
|
|
* - If true: show
|
|
|
|
* - If number: show but start at that number
|
2017-03-26 13:32:29 -07:00
|
|
|
*/
|
2018-02-28 12:05:59 -08:00
|
|
|
@Input() linenums: boolean | number | string;
|
2017-03-26 13:32:29 -07:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Path to the source of the code. */
|
|
|
|
@Input() path: string;
|
2017-04-27 22:57:34 -07:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Region of the source of the code being displayed. */
|
|
|
|
@Input() region: string;
|
2017-03-26 13:32:29 -07:00
|
|
|
|
2018-10-11 13:29:59 +02:00
|
|
|
/** Optional header to be displayed above the code. */
|
2017-05-02 11:57:26 +01:00
|
|
|
@Input()
|
2018-10-11 13:29:59 +02:00
|
|
|
set header(header: string) {
|
|
|
|
this._header = header;
|
|
|
|
this.ariaLabel = this.header ? `Copy code snippet from ${this.header}` : '';
|
2018-02-28 12:05:59 -08:00
|
|
|
}
|
2018-10-11 13:29:59 +02:00
|
|
|
get header(): string { return this._header; }
|
|
|
|
private _header: string;
|
2017-05-02 11:57:26 +01:00
|
|
|
|
2018-03-20 18:22:59 +02:00
|
|
|
@Output() codeFormatted = new EventEmitter<void>();
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** The element in the template that will display the formatted code. */
|
2017-03-26 13:32:29 -07:00
|
|
|
@ViewChild('codeContainer') codeContainer: ElementRef;
|
|
|
|
|
|
|
|
constructor(
|
2017-10-13 13:02:27 -07:00
|
|
|
private snackbar: MatSnackBar,
|
2017-03-26 13:32:29 -07:00
|
|
|
private pretty: PrettyPrinter,
|
|
|
|
private copier: CopierService,
|
|
|
|
private logger: Logger) {}
|
|
|
|
|
|
|
|
ngOnChanges() {
|
2018-02-28 12:05:59 -08:00
|
|
|
// If some inputs have changed and there is code displayed, update the view with the latest
|
|
|
|
// formatted code.
|
|
|
|
if (this.code) {
|
|
|
|
this.formatDisplayedCode();
|
2017-04-01 17:57:47 -07:00
|
|
|
}
|
2018-02-28 12:05:59 -08:00
|
|
|
}
|
2017-04-01 21:16:22 +01:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
private formatDisplayedCode() {
|
|
|
|
const leftAlignedCode = leftAlign(this.code);
|
|
|
|
this.setCodeHtml(leftAlignedCode); // start with unformatted code
|
2017-06-22 00:56:11 +03:00
|
|
|
this.codeText = this.getCodeText(); // store the unformatted code as text (for copying)
|
2018-02-28 12:05:59 -08:00
|
|
|
|
2018-03-20 18:22:59 +02:00
|
|
|
this.pretty
|
|
|
|
.formatCode(leftAlignedCode, this.language, this.getLinenums(leftAlignedCode))
|
2018-03-21 03:40:23 +02:00
|
|
|
.pipe(tap(() => this.codeFormatted.emit()))
|
2018-02-28 12:05:59 -08:00
|
|
|
.subscribe(c => this.setCodeHtml(c), err => { /* ignore failure to format */ }
|
2017-03-26 13:32:29 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Sets the message showing that the code could not be found. */
|
|
|
|
private showMissingCodeMessage() {
|
|
|
|
const src = this.path ? this.path + (this.region ? '#' + this.region : '') : '';
|
|
|
|
const srcMsg = src ? ` for\n${src}` : '.';
|
|
|
|
this.setCodeHtml(`<p class="code-missing">The code sample is missing${srcMsg}</p>`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Sets the innerHTML of the code container to the provided code string. */
|
2017-03-26 13:32:29 -07:00
|
|
|
private setCodeHtml(formattedCode: string) {
|
2018-02-28 12:05:59 -08:00
|
|
|
// **Security:** Code example content is provided by docs authors and as such its considered to
|
2017-03-26 13:32:29 -07:00
|
|
|
// be safe for innerHTML purposes.
|
|
|
|
this.codeContainer.nativeElement.innerHTML = formattedCode;
|
|
|
|
}
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Gets the textContent of the displayed code element. */
|
2017-06-22 00:56:11 +03:00
|
|
|
private getCodeText() {
|
|
|
|
// `prettify` may remove newlines, e.g. when `linenums` are on. Retrieve the content of the
|
|
|
|
// container as text, before prettifying it.
|
|
|
|
// We take the textContent because we don't want it to be HTML encoded.
|
|
|
|
return this.codeContainer.nativeElement.textContent;
|
|
|
|
}
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Copies the code snippet to the user's clipboard. */
|
2017-03-26 13:32:29 -07:00
|
|
|
doCopy() {
|
2017-06-22 00:56:11 +03:00
|
|
|
const code = this.codeText;
|
2018-02-28 12:05:59 -08:00
|
|
|
const successfullyCopied = this.copier.copyText(code);
|
|
|
|
|
|
|
|
if (successfullyCopied) {
|
2017-03-26 13:32:29 -07:00
|
|
|
this.logger.log('Copied code to clipboard:', code);
|
2018-02-28 12:05:59 -08:00
|
|
|
this.snackbar.open('Code Copied', '', { duration: 800 });
|
2017-03-26 13:32:29 -07:00
|
|
|
} else {
|
2018-03-12 11:05:36 +00:00
|
|
|
this.logger.error(new Error(`ERROR copying code to clipboard: "${code}"`));
|
2018-02-28 12:05:59 -08:00
|
|
|
this.snackbar.open('Copy failed. Please try again!', '', { duration: 800 });
|
2017-03-26 13:32:29 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
/** Gets the calculated value of linenums (boolean/number). */
|
|
|
|
getLinenums(code: string) {
|
2017-03-26 13:32:29 -07:00
|
|
|
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
|
2018-02-28 12:05:59 -08:00
|
|
|
return linenums == null || isNaN(linenums as number) ?
|
|
|
|
(code.match(/\n/g) || []).length > DEFAULT_LINE_NUMS_COUNT : linenums;
|
2017-03-26 13:32:29 -07:00
|
|
|
}
|
|
|
|
}
|
2017-04-10 22:14:40 +01:00
|
|
|
|
2018-02-28 12:05:59 -08:00
|
|
|
function leftAlign(text: string): string {
|
2017-04-10 22:14:40 +01:00
|
|
|
let indent = Number.MAX_VALUE;
|
2018-02-28 12:05:59 -08:00
|
|
|
|
2017-04-10 22:14:40 +01:00
|
|
|
const lines = text.split('\n');
|
|
|
|
lines.forEach(line => {
|
|
|
|
const lineIndent = line.search(/\S/);
|
|
|
|
if (lineIndent !== -1) {
|
|
|
|
indent = Math.min(lineIndent, indent);
|
|
|
|
}
|
|
|
|
});
|
2018-02-28 12:05:59 -08:00
|
|
|
|
2017-04-10 22:14:40 +01:00
|
|
|
return lines.map(line => line.substr(indent)).join('\n').trim();
|
|
|
|
}
|