UNPKG

ngx-highlightjs

Version:

Instant code highlighting, auto-detect language, super easy to use.

433 lines (424 loc) 20.2 kB
import * as i0 from '@angular/core'; import { InjectionToken, PLATFORM_ID, Injectable, Inject, Optional, EventEmitter, SecurityContext, Directive, Input, Output, NgModule } from '@angular/core'; import { BehaviorSubject, EMPTY, throwError, zip, from, animationFrameScheduler } from 'rxjs'; import { filter, map, take, switchMap, tap, catchError } from 'rxjs/operators'; import { isPlatformBrowser, DOCUMENT } from '@angular/common'; import * as i2 from '@angular/platform-browser'; const HIGHLIGHT_OPTIONS = new InjectionToken('HIGHLIGHT_OPTIONS'); /** * Enable usage of the library together with "trusted-types" HTTP Content-Security-Policy (CSP) * * Can be added to angular.json -> serve -> options -> headers to try it out in DEV mode * "Content-Security-Policy": "trusted-types ngx-highlightjs; require-trusted-types-for 'script'" * * Read more... * Angular Security: https://angular.io/guide/security#enforcing-trusted-types * Trusted Types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types */ let policy; function getPolicy() { var _a; if (!policy) { try { policy = (_a = window === null || window === void 0 ? void 0 : window.trustedTypes) === null || _a === void 0 ? void 0 : _a.createPolicy('ngx-highlightjs', { createHTML: (s) => s, }); } catch (_b) { // trustedTypes.createPolicy throws if called with a name that is // already registered, even in report-only mode. Until the API changes, // catch the error not to break the applications functionally. In such // cases, the code will fall back to using strings. } } return policy; } function trustedHTMLFromStringBypass(html) { var _a; return ((_a = getPolicy()) === null || _a === void 0 ? void 0 : _a.createHTML(html)) || html; } // @dynamic class HighlightLoader { constructor(doc, platformId, _options) { var _a; this.doc = doc; this._options = _options; // Stream that emits when hljs library is loaded and ready to use this._ready = new BehaviorSubject(null); this.ready = this._ready.asObservable().pipe(filter((hljs) => !!hljs), map((hljs) => hljs), take(1)); if (isPlatformBrowser(platformId)) { // Check if hljs is already available if (doc.defaultView.hljs) { this._ready.next(doc.defaultView.hljs); } else { // Load hljs library this._loadLibrary().pipe(switchMap((hljs) => { if (this._options && this._options.lineNumbersLoader) { // Make hljs available on window object (required for the line numbers library) doc.defaultView.hljs = hljs; // Load line numbers library return this.loadLineNumbers().pipe(tap(() => this._ready.next(hljs))); } else { this._ready.next(hljs); return EMPTY; } }), catchError((e) => { console.error('[HLJS] ', e); return EMPTY; })).subscribe(); } // Load highlighting theme if ((_a = this._options) === null || _a === void 0 ? void 0 : _a.themePath) { this.loadTheme(this._options.themePath); } } } /** * Lazy-Load highlight.js library */ _loadLibrary() { if (this._options) { if (this._options.fullLibraryLoader && this._options.coreLibraryLoader) { return throwError(() => 'The full library and the core library were imported, only one of them should be imported!'); } if (this._options.fullLibraryLoader && this._options.languages) { return throwError(() => 'The highlighting languages were imported they are not needed!'); } if (this._options.coreLibraryLoader && !this._options.languages) { return throwError(() => 'The highlighting languages were not imported!'); } if (!this._options.coreLibraryLoader && this._options.languages) { return throwError(() => 'The core library was not imported!'); } if (this._options.fullLibraryLoader) { return this.loadFullLibrary(); } if (this._options.coreLibraryLoader && this._options.languages && Object.keys(this._options.languages).length) { return this.loadCoreLibrary().pipe(switchMap((hljs) => this._loadLanguages(hljs))); } } return throwError(() => 'Highlight.js library was not imported!'); } /** * Lazy-load highlight.js languages */ _loadLanguages(hljs) { const languages = Object.entries(this._options.languages).map(([langName, langLoader]) => importModule(langLoader()).pipe(tap((langFunc) => hljs.registerLanguage(langName, langFunc)))); return zip(...languages).pipe(map(() => hljs)); } /** * Import highlight.js core library */ loadCoreLibrary() { return importModule(this._options.coreLibraryLoader()); } /** * Import highlight.js library with all languages */ loadFullLibrary() { return importModule(this._options.fullLibraryLoader()); } /** * Import line numbers library */ loadLineNumbers() { return importModule(this._options.lineNumbersLoader()); } /** * Reload theme styles */ setTheme(path) { this._themeLinkElement.href = path; } /** * Load theme */ loadTheme(path) { this._themeLinkElement = this.doc.createElement('link'); this._themeLinkElement.href = path; this._themeLinkElement.type = 'text/css'; this._themeLinkElement.rel = 'stylesheet'; this._themeLinkElement.media = 'screen,print'; this.doc.head.appendChild(this._themeLinkElement); } } HighlightLoader.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightLoader, deps: [{ token: DOCUMENT }, { token: PLATFORM_ID }, { token: HIGHLIGHT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); HighlightLoader.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightLoader, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightLoader, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [HIGHLIGHT_OPTIONS] }] }]; } }); /** * Map loader response to module object */ const importModule = (moduleLoader) => { return from(moduleLoader).pipe(filter((module) => !!module && !!module.default), map((module) => module.default)); }; class HighlightJS { constructor(_loader, options) { this._loader = _loader; this._hljs = null; // Load highlight.js library on init _loader.ready.subscribe((hljs) => { this._hljs = hljs; if (options && options.config) { // Set global config if present hljs.configure(options.config); if (hljs.listLanguages().length < 1) { console.error('[HighlightJS]: No languages were registered!'); } } }); } // A reference for hljs library get hljs() { return this._hljs; } /** * Core highlighting function. Accepts the code to highlight (string) and a list of options (object) * @param code Accepts the code to highlight * @param language must be present and specify the language name or alias of the grammar to be used for highlighting * @param ignoreIllegals (optional) when set to true it forces highlighting to finish even in case of detecting illegal syntax for the language instead of throwing an exception. */ highlight(code, { language, ignoreIllegals }) { return this._loader.ready.pipe(map((hljs) => hljs.highlight(code, { language, ignoreIllegals }))); } /** * Highlighting with language detection. * @param value Accepts a string with the code to highlight * @param languageSubset An optional array of language names and aliases restricting detection to only those languages. * The subset can also be set with configure, but the local parameter overrides the option if set. */ highlightAuto(value, languageSubset) { return this._loader.ready.pipe(map((hljs) => hljs.highlightAuto(value, languageSubset))); } /** * Applies highlighting to a DOM node containing code. * This function is the one to use to apply highlighting dynamically after page load or within initialization code of third-party JavaScript frameworks. * The function uses language detection by default but you can specify the language in the class attribute of the DOM node. See the scopes reference for all available language names and scopes. * @param element */ highlightElement(element) { return this._loader.ready.pipe(map((hljs) => hljs.highlightElement(element))); } /** * Applies highlighting to all elements on a page matching the configured cssSelector. The default cssSelector value is 'pre code', * which highlights all code blocks. This can be called before or after the page’s onload event has fired. */ highlightAll() { return this._loader.ready.pipe(map((hljs) => hljs.highlightAll())); } /** * @deprecated in version 12 * Configures global options: * @param config HighlightJs configuration argument */ configure(config) { return this._loader.ready.pipe(map((hljs) => hljs.configure(config))); } /** * Adds new language to the library under the specified name. Used mostly internally. * @param languageName A string with the name of the language being registered * @param languageDefinition A function that returns an object which represents the language definition. * The function is passed the hljs object to be able to use common regular expressions defined within it. */ registerLanguage(languageName, languageDefinition) { return this._loader.ready.pipe(tap((hljs) => hljs.registerLanguage(languageName, languageDefinition))); } /** * Removes a language and its aliases from the library. Used mostly internall * @param languageName: a string with the name of the language being removed. */ unregisterLanguage(languageName) { return this._loader.ready.pipe(tap((hljs) => hljs.unregisterLanguage(languageName))); } /** * Adds new language alias or aliases to the library for the specified language name defined under languageName key. * @param alias: A string or array with the name of alias being registered * @param languageName: the language name as specified by registerLanguage. */ registerAliases(alias, { languageName }) { return this._loader.ready.pipe(tap((hljs) => hljs.registerAliases(alias, { languageName }))); } /** * @return The languages names list. */ listLanguages() { return this._loader.ready.pipe(map((hljs) => hljs.listLanguages())); } /** * Looks up a language by name or alias. * @param name Language name * @return The language object if found, undefined otherwise. */ getLanguage(name) { return this._loader.ready.pipe(map((hljs) => hljs.getLanguage(name))); } /** * Enables safe mode. This is the default mode, providing the most reliable experience for production usage. */ safeMode() { return this._loader.ready.pipe(map((hljs) => hljs.safeMode())); } /** * Enables debug/development mode. */ debugMode() { return this._loader.ready.pipe(map((hljs) => hljs.debugMode())); } /** * Display line numbers * @param el Code element */ lineNumbersBlock(el) { return this._loader.ready.pipe(filter((hljs) => !!hljs.lineNumbersBlock), tap((hljs) => hljs.lineNumbersBlock(el))); } } HighlightJS.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightJS, deps: [{ token: HighlightLoader }, { token: HIGHLIGHT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); HighlightJS.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightJS, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightJS, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: HighlightLoader }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [HIGHLIGHT_OPTIONS] }] }]; } }); class Highlight { constructor(el, _hljs, _sanitizer, _options) { this._hljs = _hljs; this._sanitizer = _sanitizer; this._options = _options; // Stream that emits when code string is highlighted this.highlighted = new EventEmitter(); this._nativeElement = el.nativeElement; } ngOnChanges(changes) { var _a; if (((_a = changes === null || changes === void 0 ? void 0 : changes.code) === null || _a === void 0 ? void 0 : _a.currentValue) !== null && changes.code.currentValue !== changes.code.previousValue) { if (this.code) { this.highlightElement(this.code, this.languages); } else { // If string is empty, set the text content to empty this.setTextContent(''); } } } /** * Highlighting with language detection and fix markup. * @param code Accepts a string with the code to highlight * @param languages An optional array of language names and aliases restricting detection to only those languages. * The subset can also be set with configure, but the local parameter overrides the option if set. */ highlightElement(code, languages) { // Set code text before highlighting this.setTextContent(code); this._hljs.highlightAuto(code, languages).subscribe((res) => { // Set highlighted code this.setInnerHTML(res === null || res === void 0 ? void 0 : res.value); // Check if user want to show line numbers if (this.lineNumbers && this._options && this._options.lineNumbersLoader) { this.addLineNumbers(); } // Forward highlight response to the highlighted output this.highlighted.emit(res); }); } addLineNumbers() { // Clean up line numbers observer this.destroyLineNumbersObserver(); animationFrameScheduler.schedule(() => { // Add line numbers this._hljs.lineNumbersBlock(this._nativeElement).subscribe(); // If lines count is 1, the line numbers library will not add numbers // Observe changes to add 'hljs-line-numbers' class only when line numbers is added to the code element this._lineNumbersObs = new MutationObserver(() => { if (this._nativeElement.firstElementChild && this._nativeElement.firstElementChild.tagName.toUpperCase() === 'TABLE') { this._nativeElement.classList.add('hljs-line-numbers'); } this.destroyLineNumbersObserver(); }); this._lineNumbersObs.observe(this._nativeElement, { childList: true }); }); } destroyLineNumbersObserver() { if (this._lineNumbersObs) { this._lineNumbersObs.disconnect(); this._lineNumbersObs = null; } } setTextContent(content) { animationFrameScheduler.schedule(() => this._nativeElement.textContent = content); } setInnerHTML(content) { animationFrameScheduler.schedule(() => this._nativeElement.innerHTML = trustedHTMLFromStringBypass(this._sanitizer.sanitize(SecurityContext.HTML, content) || '')); } } Highlight.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: Highlight, deps: [{ token: i0.ElementRef }, { token: HighlightJS }, { token: i2.DomSanitizer }, { token: HIGHLIGHT_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); Highlight.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.1.0", type: Highlight, selector: "[highlight]", inputs: { code: ["highlight", "code"], languages: "languages", lineNumbers: "lineNumbers" }, outputs: { highlighted: "highlighted" }, host: { properties: { "class.hljs": "true" } }, usesOnChanges: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: Highlight, decorators: [{ type: Directive, args: [{ host: { '[class.hljs]': 'true' }, selector: '[highlight]' }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: HighlightJS }, { type: i2.DomSanitizer }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [HIGHLIGHT_OPTIONS] }] }]; }, propDecorators: { code: [{ type: Input, args: ['highlight'] }], languages: [{ type: Input }], lineNumbers: [{ type: Input }], highlighted: [{ type: Output }] } }); class HighlightModule { } HighlightModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); HighlightModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.1.0", ngImport: i0, type: HighlightModule, declarations: [Highlight], exports: [Highlight] }); HighlightModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: HighlightModule, decorators: [{ type: NgModule, args: [{ declarations: [Highlight], exports: [Highlight] }] }] }); /** * Generated bundle index. Do not edit. */ export { HIGHLIGHT_OPTIONS, Highlight, HighlightJS, HighlightLoader, HighlightModule }; //# sourceMappingURL=ngx-highlightjs.mjs.map