diff --git a/projects/ngx-highlightjs-demo/src/environments/environment.prod.ts b/projects/ngx-highlightjs-demo/src/environments/environment.prod.ts
deleted file mode 100644
index 3612073..0000000
--- a/projects/ngx-highlightjs-demo/src/environments/environment.prod.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const environment = {
- production: true
-};
diff --git a/projects/ngx-highlightjs-demo/src/environments/environment.ts b/projects/ngx-highlightjs-demo/src/environments/environment.ts
deleted file mode 100644
index 30d7bcc..0000000
--- a/projects/ngx-highlightjs-demo/src/environments/environment.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// This file can be replaced during build by using the `fileReplacements` array.
-// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
-// The list of file replacements can be found in `angular.json`.
-
-export const environment = {
- production: false
-};
-
-/*
- * For easier debugging in development mode, you can import the following file
- * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
- *
- * This import should be commented out in production mode because it will have a negative impact
- * on performance if an error is thrown.
- */
-// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/projects/ngx-highlightjs-demo/src/index.html b/projects/ngx-highlightjs-demo/src/index.html
index 3229d6a..e84a3a2 100644
--- a/projects/ngx-highlightjs-demo/src/index.html
+++ b/projects/ngx-highlightjs-demo/src/index.html
@@ -1,52 +1,66 @@
-
+
-
-
+
diff --git a/projects/ngx-highlightjs-demo/src/main.ts b/projects/ngx-highlightjs-demo/src/main.ts
index bd34a17..35b00f3 100644
--- a/projects/ngx-highlightjs-demo/src/main.ts
+++ b/projects/ngx-highlightjs-demo/src/main.ts
@@ -1,51 +1,6 @@
-import { enableProdMode } from '@angular/core';
-import { provideHttpClient } from '@angular/common/http';
-import { provideAnimations } from '@angular/platform-browser/animations';
import { bootstrapApplication } from '@angular/platform-browser';
-import { GIST_OPTIONS } from 'ngx-highlightjs/plus';
-import { HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
-import { environment } from './environments/environment';
+import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
-if (environment.production) {
- enableProdMode();
-}
-
-function bootstrap() {
- bootstrapApplication(AppComponent, {
- providers: [
- provideHttpClient(),
- {
- provide: HIGHLIGHT_OPTIONS,
- useValue: {
- // fullLibraryLoader: () => import('highlight.js'),
- lineNumbersLoader: () => import('ngx-highlightjs/line-numbers'),
- coreLibraryLoader: () => import('highlight.js/lib/core'),
- languages: {
- typescript: () => import('highlight.js/lib/languages/typescript'),
- css: () => import('highlight.js/lib/languages/css'),
- xml: () => import('highlight.js/lib/languages/xml')
- },
- themePath: 'assets/styles/androidstudio.css'
- }
- },
- {
- provide: GIST_OPTIONS,
- useValue: {
- // clientId:
- // clientSecret:
- }
- },
- provideAnimations()
- ]
- })
- .catch(err => console.error(err));
-};
-
-
-if (document.readyState === 'complete') {
- bootstrap();
-} else {
- document.addEventListener('DOMContentLoaded', bootstrap);
-}
-
+bootstrapApplication(AppComponent, appConfig)
+ .catch((err) => console.error(err));
diff --git a/projects/ngx-highlightjs-demo/src/polyfills.ts b/projects/ngx-highlightjs-demo/src/polyfills.ts
deleted file mode 100644
index dcd18ea..0000000
--- a/projects/ngx-highlightjs-demo/src/polyfills.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * This file includes polyfills needed by Angular and is loaded before the app.
- * You can add your own extra polyfills to this file.
- *
- * This file is divided into 2 sections:
- * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
- * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
- * file.
- *
- * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
- * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
- * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
- *
- * Learn more in https://angular.io/guide/browser-support
- */
-
-/***************************************************************************************************
- * BROWSER POLYFILLS
- */
-
-/**
- * By default, zone.js will patch all possible macroTask and DomEvents
- * user can disable parts of macroTask/DomEvents patch by setting following flags
- * because those flags need to be set before `zone.js` being loaded, and webpack
- * will put import in the top of bundle, so user need to create a separate file
- * in this directory (for example: zone-flags.ts), and put the following flags
- * into that file, and then add the following code before importing zone.js.
- * import './zone-flags';
- *
- * The flags allowed in zone-flags.ts are listed here.
- *
- * The following flags will work for all browsers.
- *
- * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
- * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
- * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
- *
- * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
- * with the following flag, it will bypass `zone.js` patch for IE/Edge
- *
- * (window as any).__Zone_enable_cross_context_check = true;
- *
- */
-
-/***************************************************************************************************
- * Zone JS is required by default for Angular itself.
- */
-import 'zone.js'; // Included with Angular CLI.
-
-
-/***************************************************************************************************
- * APPLICATION IMPORTS
- */
diff --git a/projects/ngx-highlightjs-demo/src/styles.scss b/projects/ngx-highlightjs-demo/src/styles.scss
index 31deb5e..5868d79 100644
--- a/projects/ngx-highlightjs-demo/src/styles.scss
+++ b/projects/ngx-highlightjs-demo/src/styles.scss
@@ -1,8 +1,5 @@
/* You can add global styles to this file, and also import other style files */
@use '@angular/material' as mat;
-@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
-@import url("https://fonts.googleapis.com/css?family=Roboto:300,500,700");
-
@include mat.core();
$my-primary: mat.define-palette(mat.$blue-gray-palette, 700);
@@ -40,3 +37,12 @@ h3 {
font-weight: 300;
margin: 3em 0 1.5em;
}
+
+html, body { height: 100%; }
+body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+
+ng-scrollbar.ng-scrollbar {
+ --scrollbar-thumb-color: orange;
+ --scrollbar-track-color: rgb(0 0 0 / 20%);
+ --scrollbar-hover-thickness: 10;
+}
diff --git a/projects/ngx-highlightjs-demo/src/test.ts b/projects/ngx-highlightjs-demo/src/test.ts
deleted file mode 100644
index ae25f27..0000000
--- a/projects/ngx-highlightjs-demo/src/test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
-
-import 'zone.js/testing';
-import { getTestBed } from '@angular/core/testing';
-import {
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
-
-// First, initialize the Angular testing environment.
-getTestBed().initTestEnvironment(
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting(), {
- teardown: { destroyAfterEach: false }
-}
-);
diff --git a/projects/ngx-highlightjs-demo/tsconfig.app.json b/projects/ngx-highlightjs-demo/tsconfig.app.json
index fd37f74..106c984 100644
--- a/projects/ngx-highlightjs-demo/tsconfig.app.json
+++ b/projects/ngx-highlightjs-demo/tsconfig.app.json
@@ -3,11 +3,14 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
- "types": []
+ "types": [
+ "node"
+ ]
},
"files": [
"src/main.ts",
- "src/polyfills.ts"
+ "src/main.server.ts",
+ "server.ts"
],
"include": [
"src/**/*.d.ts"
diff --git a/projects/ngx-highlightjs-demo/tsconfig.server.json b/projects/ngx-highlightjs-demo/tsconfig.server.json
deleted file mode 100644
index 0f56b0a..0000000
--- a/projects/ngx-highlightjs-demo/tsconfig.server.json
+++ /dev/null
@@ -1,14 +0,0 @@
-/* To learn more about this file see: https://angular.io/config/tsconfig. */
-{
- "extends": "./tsconfig.app.json",
- "compilerOptions": {
- "outDir": "../../out-tsc/server",
- "types": [
- "node"
- ]
- },
- "files": [
- "src/main.server.ts",
- "server.ts"
- ]
-}
diff --git a/projects/ngx-highlightjs-demo/tsconfig.spec.json b/projects/ngx-highlightjs-demo/tsconfig.spec.json
index b66a2f0..a9c0752 100644
--- a/projects/ngx-highlightjs-demo/tsconfig.spec.json
+++ b/projects/ngx-highlightjs-demo/tsconfig.spec.json
@@ -7,10 +7,6 @@
"jasmine"
]
},
- "files": [
- "src/test.ts",
- "src/polyfills.ts"
- ],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
diff --git a/projects/ngx-highlightjs/.eslintrc.json b/projects/ngx-highlightjs/.eslintrc.json
new file mode 100644
index 0000000..92dd4db
--- /dev/null
+++ b/projects/ngx-highlightjs/.eslintrc.json
@@ -0,0 +1,57 @@
+{
+ "root": true,
+ "ignorePatterns": [
+ "projects/**/*"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts"
+ ],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@angular-eslint/recommended",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "",
+ "style": "kebab-case"
+ }
+ ],
+ "@angular-eslint/component-class-suffix": 0,
+ "@angular-eslint/directive-class-suffix": 0,
+ "@angular-eslint/no-input-rename": 0,
+ "@angular-eslint/no-output-rename": 0,
+ "@angular-eslint/no-output-native": 0,
+ "@angular-eslint/no-host-metadata-property": 0,
+ "@typescript-eslint/no-explicit-any": 0,
+ "no-prototype-builtins": 0
+ }
+ },
+ {
+ "files": [
+ "*.html"
+ ],
+ "extends": [
+ "plugin:@angular-eslint/template/recommended",
+ "plugin:@angular-eslint/template/accessibility"
+ ],
+ "rules": {
+ "@angular-eslint/template/elements-content": 0
+ }
+ }
+ ]
+}
diff --git a/projects/ngx-highlightjs/karma.conf.js b/projects/ngx-highlightjs/karma.conf.js
index 443bd1f..9778b6c 100644
--- a/projects/ngx-highlightjs/karma.conf.js
+++ b/projects/ngx-highlightjs/karma.conf.js
@@ -13,23 +13,29 @@ module.exports = function (config) {
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
+ jasmine: {
+ // you can add configuration options for Jasmine here
+ // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
+ // for example, you can disable the random execution with `random: false`
+ // or set a specific seed with `seed: 4321`
+ },
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
+ jasmineHtmlReporter: {
+ suppressAll: true // removes the duplicated traces
+ },
coverageReporter: {
dir: require('path').join(__dirname, '../../coverage/ngx-highlightjs'),
subdir: '.',
reporters: [
{ type: 'html' },
- { type: 'text-summary' }
+ { type: 'text-summary' },
+ { type: 'cobertura' },
+ { type: 'lcov' }
]
},
reporters: ['progress', 'kjhtml'],
- port: 9876,
- colors: true,
- logLevel: config.LOG_INFO,
- autoWatch: true,
browsers: ['Chrome'],
- singleRun: false,
restartOnFileChange: true
});
};
diff --git a/projects/ngx-highlightjs/line-numbers/src/line-numbers-lib.ts b/projects/ngx-highlightjs/line-numbers/src/line-numbers-lib.ts
new file mode 100644
index 0000000..43fe756
--- /dev/null
+++ b/projects/ngx-highlightjs/line-numbers/src/line-numbers-lib.ts
@@ -0,0 +1,375 @@
+export function activateLineNumbers() {
+ const w: any = window;
+ const d: Document = document;
+
+ const TABLE_NAME: string = 'hljs-ln',
+ LINE_NAME: string = 'hljs-ln-line',
+ CODE_BLOCK_NAME: string = 'hljs-ln-code',
+ NUMBERS_BLOCK_NAME: string = 'hljs-ln-numbers',
+ NUMBER_LINE_NAME: string = 'hljs-ln-n',
+ DATA_ATTR_NAME: string = 'data-line-number',
+ BREAK_LINE_REGEXP: RegExp = /\r\n|\r|\n/g;
+
+ if (w.hljs) {
+ w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad;
+ w.hljs.lineNumbersBlock = lineNumbersBlock;
+ w.hljs.lineNumbersValue = lineNumbersValue;
+
+ addStyles();
+ } else {
+ w.console.error('highlight.js not detected!');
+ }
+
+ function isHljsLnCodeDescendant(domElt): boolean {
+ let curElt = domElt;
+ while (curElt) {
+ if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) {
+ return true;
+ }
+ curElt = curElt.parentNode;
+ }
+ return false;
+ }
+
+ function getHljsLnTable(hljsLnDomElt) {
+ let curElt = hljsLnDomElt;
+ while (curElt.nodeName !== 'TABLE') {
+ curElt = curElt.parentNode;
+ }
+ return curElt;
+ }
+
+ // Function to workaround a copy issue with Microsoft Edge.
+ // Due to hljs-ln wrapping the lines of code inside a
element,
+ // itself wrapped inside a element, window.getSelection().toString()
+ // does not contain any line breaks. So we need to get them back using the
+ // rendered code in the DOM as reference.
+ function edgeGetSelectedCodeLines(selection) {
+ // current selected text without line breaks
+ const selectionText = selection.toString();
+
+ // get the element wrapping the first line of selected code
+ let tdAnchor = selection.anchorNode;
+ while (tdAnchor.nodeName !== 'TD') {
+ tdAnchor = tdAnchor.parentNode;
+ }
+
+ // get the | element wrapping the last line of selected code
+ let tdFocus = selection.focusNode;
+ while (tdFocus.nodeName !== 'TD') {
+ tdFocus = tdFocus.parentNode;
+ }
+
+ // extract line numbers
+ let firstLineNumber = parseInt(tdAnchor.dataset.lineNumber);
+ let lastLineNumber = parseInt(tdFocus.dataset.lineNumber);
+
+ // multi-lines copied case
+ if (firstLineNumber != lastLineNumber) {
+
+ let firstLineText = tdAnchor.textContent;
+ let lastLineText = tdFocus.textContent;
+
+ // if the selection was made backward, swap values
+ if (firstLineNumber > lastLineNumber) {
+ let tmp = firstLineNumber;
+ firstLineNumber = lastLineNumber;
+ lastLineNumber = tmp;
+ tmp = firstLineText;
+ firstLineText = lastLineText;
+ lastLineText = tmp;
+ }
+
+ // discard not copied characters in first line
+ while (selectionText.indexOf(firstLineText) !== 0) {
+ firstLineText = firstLineText.slice(1);
+ }
+
+ // discard not copied characters in last line
+ while (selectionText.lastIndexOf(lastLineText) === -1) {
+ lastLineText = lastLineText.slice(0, -1);
+ }
+
+ // reconstruct and return the real copied text
+ let selectedText = firstLineText;
+ const hljsLnTable = getHljsLnTable(tdAnchor);
+ for (let i: number = firstLineNumber + 1; i < lastLineNumber; ++i) {
+ const codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]);
+ const codeLineElt = hljsLnTable.querySelector(codeLineSel);
+ selectedText += '\n' + codeLineElt.textContent;
+ }
+ selectedText += '\n' + lastLineText;
+ return selectedText;
+ // single copied line case
+ } else {
+ return selectionText;
+ }
+ }
+
+ // ensure consistent code copy/paste behavior across all browsers
+ // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51)
+ document.addEventListener('copy', function (e: ClipboardEvent) {
+ // get current selection
+ const selection: Selection = window.getSelection();
+ // override behavior when one wants to copy line of codes
+ if (isHljsLnCodeDescendant(selection.anchorNode)) {
+ let selectionText;
+ // workaround an issue with Microsoft Edge as copied line breaks
+ // are removed otherwise from the selection string
+ if (window.navigator.userAgent.indexOf('Edge') !== -1) {
+ selectionText = edgeGetSelectedCodeLines(selection);
+ } else {
+ // other browsers can directly use the selection string
+ selectionText = selection.toString();
+ }
+ e.clipboardData.setData('text/plain', selectionText);
+ e.preventDefault();
+ }
+ });
+
+ function addStyles() {
+ const css: HTMLStyleElement = d.createElement('style');
+ css.type = 'text/css';
+ css.innerHTML = format(
+ '.{0}{border-collapse:collapse}' +
+ '.{0} td{padding:0}' +
+ '.{1}:before{content:attr({2})}',
+ [
+ TABLE_NAME,
+ NUMBER_LINE_NAME,
+ DATA_ATTR_NAME
+ ]);
+ d.getElementsByTagName('head')[0].appendChild(css);
+ }
+
+ function initLineNumbersOnLoad(options) {
+ if (d.readyState === 'interactive' || d.readyState === 'complete') {
+ documentReady(options);
+ } else {
+ w.addEventListener('DOMContentLoaded', function () {
+ documentReady(options);
+ });
+ }
+ }
+
+ function documentReady(options): void {
+ try {
+ const blocks: NodeListOf = d.querySelectorAll('code.hljs,code.nohighlight');
+
+ for (const i in blocks) {
+ if (blocks.hasOwnProperty(i)) {
+ if (!isPluginDisabledForBlock(blocks[i])) {
+ lineNumbersBlock(blocks[i], options);
+ }
+ }
+ }
+ } catch (e) {
+ w.console.error('LineNumbers error: ', e);
+ }
+ }
+
+ function isPluginDisabledForBlock(element) {
+ return element.classList.contains('nohljsln');
+ }
+
+ function lineNumbersBlock(element, options) {
+ if (typeof element !== 'object') {
+ return;
+ }
+
+ async(function () {
+ element.innerHTML = lineNumbersInternal(element, options);
+ });
+ }
+
+ function lineNumbersValue(value, options) {
+ if (typeof value !== 'string') {
+ return;
+ }
+
+ const element: HTMLElement = document.createElement('code');
+ element.innerHTML = value;
+
+ return lineNumbersInternal(element, options);
+ }
+
+ function lineNumbersInternal(element, options) {
+
+ const internalOptions = mapOptions(element, options);
+
+ duplicateMultilineNodes(element);
+
+ return addLineNumbersBlockFor(element.innerHTML, internalOptions);
+ }
+
+ function addLineNumbersBlockFor(inputHtml, options) {
+ const lines = getLines(inputHtml);
+
+ // if last line contains only carriage return remove it
+ if (lines[lines.length - 1].trim() === '') {
+ lines.pop();
+ }
+
+ if (lines.length > 1 || options.singleLine) {
+ let html = '';
+
+ for (let i = 0, l = lines.length; i < l; i++) {
+ html += format(
+ '' +
+ '' +
+ '' +
+ ' | ' +
+ '' +
+ '{6}' +
+ ' | ' +
+ ' ',
+ [
+ LINE_NAME,
+ NUMBERS_BLOCK_NAME,
+ NUMBER_LINE_NAME,
+ DATA_ATTR_NAME,
+ CODE_BLOCK_NAME,
+ i + options.startFrom,
+ lines[i].length > 0 ? lines[i] : ' '
+ ]);
+ }
+
+ return format('', [TABLE_NAME, html]);
+ }
+
+ return inputHtml;
+ }
+
+ /**
+ * @param {HTMLElement} element Code block.
+ * @param {Object} options External API options.
+ * @returns {Object} Internal API options.
+ */
+ function mapOptions(element, options) {
+ options = options || {};
+ return {
+ singleLine: getSingleLineOption(options),
+ startFrom: getStartFromOption(element, options)
+ };
+ }
+
+ function getSingleLineOption(options) {
+ const defaultValue: boolean = false;
+ if (options.singleLine) {
+ return options.singleLine;
+ }
+ return defaultValue;
+ }
+
+ function getStartFromOption(element, options) {
+ const defaultValue: number = 1;
+ let startFrom: number = defaultValue;
+
+ if (isFinite(options.startFrom)) {
+ startFrom = options.startFrom;
+ }
+
+ // can be overridden because local option is priority
+ const value = getAttribute(element, 'data-ln-start-from');
+ if (value !== null) {
+ startFrom = toNumber(value, defaultValue);
+ }
+
+ return startFrom;
+ }
+
+ /**
+ * Recursive method for fix multi-line elements implementation in highlight.js
+ * Doing deep passage on child nodes.
+ * @param {HTMLElement} element
+ */
+ function duplicateMultilineNodes(element) {
+ const nodes = element.childNodes;
+ for (const node in nodes) {
+ if (nodes.hasOwnProperty(node)) {
+ const child = nodes[node];
+ if (getLinesCount(child.textContent) > 0) {
+ if (child.childNodes.length > 0) {
+ duplicateMultilineNodes(child);
+ } else {
+ duplicateMultilineNode(child.parentNode);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Method for fix multi-line elements implementation in highlight.js
+ * @param {HTMLElement} element
+ */
+ function duplicateMultilineNode(element) {
+ const className = element.className;
+
+ if (!/hljs-/.test(className)) {
+ return;
+ }
+
+ const lines = getLines(element.innerHTML);
+ let result: string = '';
+
+ for (let i: number = 0; i < lines.length; i++) {
+ const lineText = lines[i].length > 0 ? lines[i] : ' ';
+ result += format('{1}\n', [className, lineText]);
+ }
+
+ element.innerHTML = result.trim();
+ }
+
+ function getLines(text) {
+ if (text.length === 0) {
+ return [];
+ }
+ return text.split(BREAK_LINE_REGEXP);
+ }
+
+ function getLinesCount(text) {
+ return (text.trim().match(BREAK_LINE_REGEXP) || []).length;
+ }
+
+ ///
+ /// HELPERS
+ ///
+
+ function async(func) {
+ w.setTimeout(func, 0);
+ }
+
+ /**
+ * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript}
+ * @param {string} format
+ * @param {array} args
+ */
+ function format(format, args) {
+ return format.replace(/\{(\d+)\}/g, function (m, n) {
+ return args[n] !== undefined ? args[n] : m;
+ });
+ }
+
+ /**
+ * @param {HTMLElement} element Code block.
+ * @param {String} attrName Attribute name.
+ * @returns {String} Attribute value or empty.
+ */
+ function getAttribute(element, attrName) {
+ return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null;
+ }
+
+ /**
+ * @param {String} str Source string.
+ * @param {Number} fallback Fallback value.
+ * @returns Parsed number or fallback value.
+ */
+ function toNumber(str, fallback) {
+ if (!str) {
+ return fallback;
+ }
+ const number: number = Number(str);
+ return isFinite(number) ? number : fallback;
+ }
+}
diff --git a/projects/ngx-highlightjs/line-numbers/src/line-numbers.ts b/projects/ngx-highlightjs/line-numbers/src/line-numbers.ts
index 94f990c..2d2f4db 100644
--- a/projects/ngx-highlightjs/line-numbers/src/line-numbers.ts
+++ b/projects/ngx-highlightjs/line-numbers/src/line-numbers.ts
@@ -1,374 +1,70 @@
-export function activateLineNumbers() {
- const w: any = window;
- const d: Document = document;
-
- var TABLE_NAME = 'hljs-ln',
- LINE_NAME = 'hljs-ln-line',
- CODE_BLOCK_NAME = 'hljs-ln-code',
- NUMBERS_BLOCK_NAME = 'hljs-ln-numbers',
- NUMBER_LINE_NAME = 'hljs-ln-n',
- DATA_ATTR_NAME = 'data-line-number',
- BREAK_LINE_REGEXP = /\r\n|\r|\n/g;
-
- if (w.hljs) {
- w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad;
- w.hljs.lineNumbersBlock = lineNumbersBlock;
- w.hljs.lineNumbersValue = lineNumbersValue;
-
- addStyles();
- } else {
- w.console.error('highlight.js not detected!');
- }
-
- function isHljsLnCodeDescendant(domElt) {
- var curElt = domElt;
- while (curElt) {
- if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) {
- return true;
- }
- curElt = curElt.parentNode;
- }
- return false;
- }
-
- function getHljsLnTable(hljsLnDomElt) {
- var curElt = hljsLnDomElt;
- while (curElt.nodeName !== 'TABLE') {
- curElt = curElt.parentNode;
- }
- return curElt;
- }
-
- // Function to workaround a copy issue with Microsoft Edge.
- // Due to hljs-ln wrapping the lines of code inside a element,
- // itself wrapped inside a element, window.getSelection().toString()
- // does not contain any line breaks. So we need to get them back using the
- // rendered code in the DOM as reference.
- function edgeGetSelectedCodeLines(selection) {
- // current selected text without line breaks
- var selectionText = selection.toString();
-
- // get the element wrapping the first line of selected code
- var tdAnchor = selection.anchorNode;
- while (tdAnchor.nodeName !== 'TD') {
- tdAnchor = tdAnchor.parentNode;
- }
-
- // get the | element wrapping the last line of selected code
- var tdFocus = selection.focusNode;
- while (tdFocus.nodeName !== 'TD') {
- tdFocus = tdFocus.parentNode;
- }
-
- // extract line numbers
- var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber);
- var lastLineNumber = parseInt(tdFocus.dataset.lineNumber);
-
- // multi-lines copied case
- if (firstLineNumber != lastLineNumber) {
-
- var firstLineText = tdAnchor.textContent;
- var lastLineText = tdFocus.textContent;
-
- // if the selection was made backward, swap values
- if (firstLineNumber > lastLineNumber) {
- var tmp = firstLineNumber;
- firstLineNumber = lastLineNumber;
- lastLineNumber = tmp;
- tmp = firstLineText;
- firstLineText = lastLineText;
- lastLineText = tmp;
- }
-
- // discard not copied characters in first line
- while (selectionText.indexOf(firstLineText) !== 0) {
- firstLineText = firstLineText.slice(1);
- }
-
- // discard not copied characters in last line
- while (selectionText.lastIndexOf(lastLineText) === -1) {
- lastLineText = lastLineText.slice(0, -1);
- }
-
- // reconstruct and return the real copied text
- var selectedText = firstLineText;
- var hljsLnTable = getHljsLnTable(tdAnchor);
- for (var i = firstLineNumber + 1; i < lastLineNumber; ++i) {
- var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]);
- var codeLineElt = hljsLnTable.querySelector(codeLineSel);
- selectedText += '\n' + codeLineElt.textContent;
- }
- selectedText += '\n' + lastLineText;
- return selectedText;
- // single copied line case
- } else {
- return selectionText;
- }
- }
-
- // ensure consistent code copy/paste behavior across all browsers
- // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51)
- document.addEventListener('copy', function(e) {
- // get current selection
- var selection = window.getSelection();
- // override behavior when one wants to copy line of codes
- if (isHljsLnCodeDescendant(selection.anchorNode)) {
- var selectionText;
- // workaround an issue with Microsoft Edge as copied line breaks
- // are removed otherwise from the selection string
- if (window.navigator.userAgent.indexOf('Edge') !== -1) {
- selectionText = edgeGetSelectedCodeLines(selection);
- } else {
- // other browsers can directly use the selection string
- selectionText = selection.toString();
- }
- e.clipboardData.setData('text/plain', selectionText);
- e.preventDefault();
- }
- });
-
- function addStyles() {
- var css = d.createElement('style');
- css.type = 'text/css';
- css.innerHTML = format(
- '.{0}{border-collapse:collapse}' +
- '.{0} td{padding:0}' +
- '.{1}:before{content:attr({2})}',
- [
- TABLE_NAME,
- NUMBER_LINE_NAME,
- DATA_ATTR_NAME
- ]);
- d.getElementsByTagName('head')[0].appendChild(css);
- }
-
- function initLineNumbersOnLoad(options) {
- if (d.readyState === 'interactive' || d.readyState === 'complete') {
- documentReady(options);
- } else {
- w.addEventListener('DOMContentLoaded', function() {
- documentReady(options);
- });
- }
- }
-
- function documentReady(options) {
- try {
- var blocks = d.querySelectorAll('code.hljs,code.nohighlight');
-
- for (var i in blocks) {
- if (blocks.hasOwnProperty(i)) {
- if (!isPluginDisabledForBlock(blocks[i])) {
- lineNumbersBlock(blocks[i], options);
- }
+import {
+ Directive,
+ Input,
+ inject,
+ effect,
+ numberAttribute,
+ booleanAttribute,
+ ElementRef,
+ PLATFORM_ID
+} from '@angular/core';
+import { isPlatformBrowser } from '@angular/common';
+import { HighlightJS, HighlightBase, HIGHLIGHT_OPTIONS, LineNumbersOptions } from 'ngx-highlightjs';
+
+@Directive({
+ standalone: true,
+ selector: '[highlight][lineNumbers], [highlightAuto][lineNumbers]'
+})
+export class HighlightLineNumbers {
+
+ private readonly _platform: object = inject(PLATFORM_ID);
+ private readonly options: LineNumbersOptions = inject(HIGHLIGHT_OPTIONS)?.lineNumbersOptions;
+ private readonly _hljs: HighlightJS = inject(HighlightJS);
+ private readonly _highlight: HighlightBase = inject(HighlightBase);
+ private readonly _nativeElement: HTMLElement = inject(ElementRef).nativeElement;
+
+ // Temp observer to observe when line numbers has been added to code element
+ private _lineNumbersObs: MutationObserver;
+
+ @Input({ transform: numberAttribute }) startFrom: number = this.options?.startFrom;
+
+ @Input({ transform: booleanAttribute }) singleLine: boolean = this.options?.singleLine;
+
+ constructor() {
+ if (isPlatformBrowser(this._platform)) {
+ effect(() => {
+ if (this._highlight.highlightResult()) {
+ this.addLineNumbers();
}
- }
- } catch (e) {
- w.console.error('LineNumbers error: ', e);
- }
- }
-
- function isPluginDisabledForBlock(element) {
- return element.classList.contains('nohljsln');
- }
-
- function lineNumbersBlock(element, options) {
- if (typeof element !== 'object') {
- return;
- }
-
- async(function() {
- element.innerHTML = lineNumbersInternal(element, options);
- });
- }
-
- function lineNumbersValue(value, options) {
- if (typeof value !== 'string') {
- return;
- }
-
- var element = document.createElement('code');
- element.innerHTML = value;
-
- return lineNumbersInternal(element, options);
- }
-
- function lineNumbersInternal(element, options) {
-
- var internalOptions = mapOptions(element, options);
-
- duplicateMultilineNodes(element);
-
- return addLineNumbersBlockFor(element.innerHTML, internalOptions);
- }
-
- function addLineNumbersBlockFor(inputHtml, options) {
- var lines = getLines(inputHtml);
-
- // if last line contains only carriage return remove it
- if (lines[lines.length - 1].trim() === '') {
- lines.pop();
- }
-
- if (lines.length > 1 || options.singleLine) {
- var html = '';
-
- for (var i = 0, l = lines.length; i < l; i++) {
- html += format(
- '' +
- '' +
- '' +
- ' | ' +
- '' +
- '{6}' +
- ' | ' +
- ' ',
- [
- LINE_NAME,
- NUMBERS_BLOCK_NAME,
- NUMBER_LINE_NAME,
- DATA_ATTR_NAME,
- CODE_BLOCK_NAME,
- i + options.startFrom,
- lines[i].length > 0 ? lines[i] : ' '
- ]);
- }
-
- return format('', [TABLE_NAME, html]);
- }
-
- return inputHtml;
- }
-
- /**
- * @param {HTMLElement} element Code block.
- * @param {Object} options External API options.
- * @returns {Object} Internal API options.
- */
- function mapOptions(element, options) {
- options = options || {};
- return {
- singleLine: getSingleLineOption(options),
- startFrom: getStartFromOption(element, options)
- };
- }
-
- function getSingleLineOption(options) {
- var defaultValue = false;
- if (!!options.singleLine) {
- return options.singleLine;
- }
- return defaultValue;
- }
-
- function getStartFromOption(element, options) {
- var defaultValue = 1;
- var startFrom = defaultValue;
-
- if (isFinite(options.startFrom)) {
- startFrom = options.startFrom;
- }
-
- // can be overridden because local option is priority
- var value = getAttribute(element, 'data-ln-start-from');
- if (value !== null) {
- startFrom = toNumber(value, defaultValue);
+ });
}
-
- return startFrom;
}
- /**
- * Recursive method for fix multi-line elements implementation in highlight.js
- * Doing deep passage on child nodes.
- * @param {HTMLElement} element
- */
- function duplicateMultilineNodes(element) {
- var nodes = element.childNodes;
- for (var node in nodes) {
- if (nodes.hasOwnProperty(node)) {
- var child = nodes[node];
- if (getLinesCount(child.textContent) > 0) {
- if (child.childNodes.length > 0) {
- duplicateMultilineNodes(child);
- } else {
- duplicateMultilineNode(child.parentNode);
- }
+ private addLineNumbers(): void {
+ // Clean up line numbers observer
+ this.destroyLineNumbersObserver();
+ requestAnimationFrame(async () => {
+ // Add line numbers
+ await this._hljs.lineNumbersBlock(this._nativeElement, {
+ startFrom: this.startFrom,
+ singleLine: this.singleLine
+ });
+ // 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?.tagName.toUpperCase() === 'TABLE') {
+ this._nativeElement.classList.add('hljs-line-numbers');
}
- }
- }
- }
-
- /**
- * Method for fix multi-line elements implementation in highlight.js
- * @param {HTMLElement} element
- */
- function duplicateMultilineNode(element) {
- var className = element.className;
-
- if (!/hljs-/.test(className)) {
- return;
- }
-
- var lines = getLines(element.innerHTML);
-
- for (var i = 0, result = ''; i < lines.length; i++) {
- var lineText = lines[i].length > 0 ? lines[i] : ' ';
- result += format('{1}\n', [className, lineText]);
- }
-
- element.innerHTML = result.trim();
- }
-
- function getLines(text) {
- if (text.length === 0) {
- return [];
- }
- return text.split(BREAK_LINE_REGEXP);
- }
-
- function getLinesCount(text) {
- return (text.trim().match(BREAK_LINE_REGEXP) || []).length;
- }
-
- ///
- /// HELPERS
- ///
-
- function async(func) {
- w.setTimeout(func, 0);
- }
-
- /**
- * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript}
- * @param {string} format
- * @param {array} args
- */
- function format(format, args) {
- return format.replace(/\{(\d+)\}/g, function(m, n) {
- return args[n] !== undefined ? args[n] : m;
+ this.destroyLineNumbersObserver();
+ });
+ this._lineNumbersObs.observe(this._nativeElement, { childList: true });
});
}
- /**
- * @param {HTMLElement} element Code block.
- * @param {String} attrName Attribute name.
- * @returns {String} Attribute value or empty.
- */
- function getAttribute(element, attrName) {
- return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null;
- }
-
- /**
- * @param {String} str Source string.
- * @param {Number} fallback Fallback value.
- * @returns Parsed number or fallback value.
- */
- function toNumber(str, fallback) {
- if (!str) {
- return fallback;
+ private destroyLineNumbersObserver(): void {
+ if (this._lineNumbersObs) {
+ this._lineNumbersObs.disconnect();
+ this._lineNumbersObs = null;
}
- var number = Number(str);
- return isFinite(number) ? number : fallback;
}
}
diff --git a/projects/ngx-highlightjs/line-numbers/src/public_api.ts b/projects/ngx-highlightjs/line-numbers/src/public_api.ts
index d03ee4c..d0932b9 100644
--- a/projects/ngx-highlightjs/line-numbers/src/public_api.ts
+++ b/projects/ngx-highlightjs/line-numbers/src/public_api.ts
@@ -1 +1,2 @@
+export * from './line-numbers-lib';
export * from './line-numbers';
diff --git a/projects/ngx-highlightjs/ng-package.json b/projects/ngx-highlightjs/ng-package.json
index 4829683..2d1fbe4 100644
--- a/projects/ngx-highlightjs/ng-package.json
+++ b/projects/ngx-highlightjs/ng-package.json
@@ -3,8 +3,5 @@
"dest": "../../dist/ngx-highlightjs",
"lib": {
"entryFile": "src/public-api.ts"
- },
- "allowedNonPeerDependencies": [
- "."
- ]
-}
+ }
+}
\ No newline at end of file
diff --git a/projects/ngx-highlightjs/package.json b/projects/ngx-highlightjs/package.json
index 00f2643..82e1e36 100644
--- a/projects/ngx-highlightjs/package.json
+++ b/projects/ngx-highlightjs/package.json
@@ -1,6 +1,6 @@
{
"name": "ngx-highlightjs",
- "version": "10.0.0",
+ "version": "12.0.0-beta.1",
"description": "Instant code highlighting, auto-detect language, super easy to use.",
"homepage": "http://github.com/murhafsousli/ngx-highlightjs",
"author": {
@@ -24,13 +24,12 @@
"gist"
],
"license": "MIT",
+ "peerDependencies": {
+ "@angular/common": ">=17.0.0",
+ "@angular/core": ">=17.0.0"
+ },
"dependencies": {
- "highlight.js": "^11.8.0",
- "tslib": "^2.0.0"
+ "tslib": "^2.3.0"
},
- "peerDependencies": {
- "@angular/common": ">=16.0.0",
- "@angular/core": ">=16.0.0",
- "rxjs": ">=7.0.0"
- }
+ "sideEffects": false
}
diff --git a/projects/ngx-highlightjs/plus/src/code-from-url.ts b/projects/ngx-highlightjs/plus/src/code-from-url.ts
index dcf7cc3..79e8016 100644
--- a/projects/ngx-highlightjs/plus/src/code-from-url.ts
+++ b/projects/ngx-highlightjs/plus/src/code-from-url.ts
@@ -1,15 +1,14 @@
-import { Pipe, PipeTransform } from '@angular/core';
+import { inject, Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs';
import { CodeLoader } from './code-loader';
@Pipe({
- name: 'codeFromUrl',
- standalone: true
+ standalone: true,
+ name: 'codeFromUrl'
})
export class CodeFromUrlPipe implements PipeTransform {
- constructor(private _loader: CodeLoader) {
- }
+ private _loader: CodeLoader = inject(CodeLoader);
transform(url: string): Observable {
return this._loader.getCodeFromUrl(url);
diff --git a/projects/ngx-highlightjs/plus/src/code-loader.ts b/projects/ngx-highlightjs/plus/src/code-loader.ts
index c0070bd..31f30cc 100644
--- a/projects/ngx-highlightjs/plus/src/code-loader.ts
+++ b/projects/ngx-highlightjs/plus/src/code-loader.ts
@@ -1,4 +1,4 @@
-import { Inject, Injectable, Optional } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, EMPTY, catchError, shareReplay } from 'rxjs';
import { Gist, GIST_OPTIONS, GistOptions } from './gist.model';
@@ -7,8 +7,10 @@ import { Gist, GIST_OPTIONS, GistOptions } from './gist.model';
providedIn: 'root'
})
export class CodeLoader {
- constructor(private _http: HttpClient, @Optional() @Inject(GIST_OPTIONS) private _options: GistOptions) {
- }
+
+ private _http: HttpClient = inject(HttpClient);
+
+ private _options: GistOptions = inject(GIST_OPTIONS, { optional: true });
/**
* Get plus code
@@ -54,7 +56,7 @@ export class CodeLoader {
}
-function isUrl(url: string) {
- const regExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
+function isUrl(url: string): boolean {
+ const regExp: RegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!]))?/;
return regExp.test(url);
}
diff --git a/projects/ngx-highlightjs/plus/src/gist.model.ts b/projects/ngx-highlightjs/plus/src/gist.model.ts
index c3d90fc..4306aec 100644
--- a/projects/ngx-highlightjs/plus/src/gist.model.ts
+++ b/projects/ngx-highlightjs/plus/src/gist.model.ts
@@ -5,7 +5,7 @@ export interface GistOptions {
clientSecret: string;
}
-export const GIST_OPTIONS = new InjectionToken('GIST_OPTIONS');
+export const GIST_OPTIONS: InjectionToken = new InjectionToken('GIST_OPTIONS');
interface Owner {
login: string;
diff --git a/projects/ngx-highlightjs/plus/src/gist.ts b/projects/ngx-highlightjs/plus/src/gist.ts
index 71163f7..8548c1b 100644
--- a/projects/ngx-highlightjs/plus/src/gist.ts
+++ b/projects/ngx-highlightjs/plus/src/gist.ts
@@ -1,15 +1,14 @@
-import { Directive, Pipe, Input, Output, PipeTransform, EventEmitter } from '@angular/core';
+import { Directive, Pipe, Input, Output, PipeTransform, EventEmitter, inject } from '@angular/core';
import { CodeLoader } from './code-loader';
import { Gist } from './gist.model';
@Directive({
- selector: '[gist]',
- standalone: true
+ standalone: true,
+ selector: '[gist]'
})
export class GistDirective {
- constructor(private _loader: CodeLoader) {
- }
+ private _loader: CodeLoader = inject(CodeLoader);
@Input()
set gist(value: string) {
@@ -22,8 +21,8 @@ export class GistDirective {
}
@Pipe({
- name: 'gistFile',
- standalone: true
+ standalone: true,
+ name: 'gistFile'
})
export class GistFilePipe implements PipeTransform {
transform(gist: Gist, fileName: string): string | null {
diff --git a/projects/ngx-highlightjs/src/lib/highlight-auto.ts b/projects/ngx-highlightjs/src/lib/highlight-auto.ts
new file mode 100644
index 0000000..170b3cc
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/highlight-auto.ts
@@ -0,0 +1,33 @@
+import { Directive, Input, Output, signal, input, EventEmitter, WritableSignal, InputSignal } from '@angular/core';
+import type { AutoHighlightResult } from 'highlight.js';
+import { HighlightBase } from './highlight-base';
+
+@Directive({
+ standalone: true,
+ selector: '[highlightAuto]',
+ providers: [{ provide: HighlightBase, useExisting: HighlightAuto }],
+ host: {
+ '[class.hljs]': 'true'
+ }
+})
+export class HighlightAuto extends HighlightBase {
+
+ // Code to highlight
+ code: InputSignal = input(null, { alias: 'highlightAuto' });
+
+ // Highlighted result
+ highlightResult: WritableSignal = signal(null);
+
+ // 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.
+ @Input() languages!: string[];
+
+ // Stream that emits when code string is highlighted
+ @Output() highlighted: EventEmitter = new EventEmitter();
+
+ protected async highlightElement(code: string): Promise {
+ const res: AutoHighlightResult = await this._hljs.highlightAuto(code, this.languages);
+ this.highlightResult.set(res);
+ }
+}
+
diff --git a/projects/ngx-highlightjs/src/lib/highlight-base.ts b/projects/ngx-highlightjs/src/lib/highlight-base.ts
new file mode 100644
index 0000000..39b9c25
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/highlight-base.ts
@@ -0,0 +1,72 @@
+import {
+ Directive,
+ inject,
+ effect,
+ ElementRef,
+ InputSignal,
+ WritableSignal,
+ SecurityContext,
+ EventEmitter,
+ PLATFORM_ID
+} from '@angular/core';
+import { isPlatformBrowser } from '@angular/common';
+import { DomSanitizer } from '@angular/platform-browser';
+import type { AutoHighlightResult, HighlightResult } from 'highlight.js';
+import { HighlightJS } from './highlight.service';
+import { trustedHTMLFromStringBypass } from './trusted-types';
+
+@Directive()
+export abstract class HighlightBase {
+
+ protected _hljs: HighlightJS = inject(HighlightJS);
+
+ private readonly _nativeElement: HTMLElement = inject(ElementRef).nativeElement;
+ private _sanitizer: DomSanitizer = inject(DomSanitizer);
+ private _platform: object = inject(PLATFORM_ID);
+
+ // Code to highlight
+ abstract code: InputSignal;
+
+ // Highlighted result
+ abstract highlightResult: WritableSignal;
+
+ // Stream that emits when code string is highlighted
+ abstract highlighted: EventEmitter;
+
+
+ constructor() {
+ if (isPlatformBrowser(this._platform)) {
+ effect(() => {
+ const code: string = this.code();
+ // Set code text before highlighting
+ this.setTextContent(code || '');
+ if (code) {
+ this.highlightElement(code);
+ }
+ });
+
+ effect(() => {
+ const res: AutoHighlightResult = this.highlightResult();
+ this.setInnerHTML(res?.value);
+ // Forward highlight response to the highlighted output
+ this.highlighted.emit(res);
+ });
+ }
+ }
+
+ protected abstract highlightElement(code: string): Promise ;
+
+ private setTextContent(content: string): void {
+ requestAnimationFrame(() =>
+ this._nativeElement.textContent = content
+ );
+ }
+
+ private setInnerHTML(content: string | null): void {
+ requestAnimationFrame(() =>
+ this._nativeElement.innerHTML = trustedHTMLFromStringBypass(
+ this._sanitizer.sanitize(SecurityContext.HTML, content) || ''
+ )
+ );
+ }
+}
diff --git a/projects/ngx-highlightjs/src/lib/highlight.loader.spec.ts b/projects/ngx-highlightjs/src/lib/highlight.loader.spec.ts
deleted file mode 100644
index 954c751..0000000
--- a/projects/ngx-highlightjs/src/lib/highlight.loader.spec.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { TestBed, waitForAsync } from '@angular/core/testing';
-
-import { BehaviorSubject } from 'rxjs';
-import * as hljs from 'highlight.js';
-import { HighlightLoader } from './highlight.loader';
-import { HighlightLibrary } from './highlight.model';
-
-
-// Fake Highlight Loader
-const highlightLoaderStub = {
- ready: new BehaviorSubject(hljs)
-};
-
-describe('HighlightService', () => {
-
- let loader: HighlightLoader;
-
- beforeEach(waitForAsync(() => {
- TestBed.configureTestingModule({
- providers: [{ provide: HighlightLoader, useValue: highlightLoaderStub }]
- }).compileComponents();
- loader = TestBed.inject(HighlightLoader);
- }));
-
- it('should load the library', (done: DoneFn) => {
- loader.ready.subscribe((lib: HighlightLibrary) => {
- expect(lib).toBeTruthy();
- done();
- });
- });
-});
diff --git a/projects/ngx-highlightjs/src/lib/highlight.loader.ts b/projects/ngx-highlightjs/src/lib/highlight.loader.ts
index cf81dae..f685046 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.loader.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.loader.ts
@@ -1,36 +1,53 @@
-import { Injectable, Inject, PLATFORM_ID, Optional } from '@angular/core';
+import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
-import { BehaviorSubject, Observable, EMPTY, from, zip, throwError, catchError, tap, map, switchMap, filter, take } from 'rxjs';
-import { HIGHLIGHT_OPTIONS, HighlightLibrary, HighlightOptions } from './highlight.model';
+import {
+ Observable,
+ BehaviorSubject,
+ EMPTY,
+ tap,
+ map,
+ from,
+ filter,
+ forkJoin,
+ switchMap,
+ throwError,
+ catchError,
+ firstValueFrom
+} from 'rxjs';
+import { HIGHLIGHT_OPTIONS, HighlightOptions } from './highlight.model';
+import type { HLJSApi } from 'highlight.js';
+import { LoaderErrors } from './loader-errors';
-// @dynamic
@Injectable({
providedIn: 'root'
})
export class HighlightLoader {
+
+ private document: Document = inject(DOCUMENT);
+ private isPlatformBrowser: boolean = isPlatformBrowser(inject(PLATFORM_ID));
+ private options: HighlightOptions = inject(HIGHLIGHT_OPTIONS, { optional: true });
+
// Stream that emits when hljs library is loaded and ready to use
- private readonly _ready: BehaviorSubject = new BehaviorSubject(null);
- readonly ready: Observable = this._ready.asObservable().pipe(
- filter((hljs: HighlightLibrary | null) => !!hljs),
- take(1)
- );
+ private readonly _ready: BehaviorSubject = new BehaviorSubject(null);
+
+ readonly ready: Promise = firstValueFrom(this._ready.asObservable().pipe(
+ filter((hljs: HLJSApi) => !!hljs),
+ ));
private _themeLinkElement: HTMLLinkElement;
- constructor(@Inject(DOCUMENT) private doc: any,
- @Inject(PLATFORM_ID) private platformId: object,
- @Optional() @Inject(HIGHLIGHT_OPTIONS) private _options: HighlightOptions) {
- if (isPlatformBrowser(platformId)) {
+ constructor() {
+ if (this.isPlatformBrowser) {
// Check if hljs is already available
- if (doc.defaultView.hljs) {
- this._ready.next(doc.defaultView.hljs);
+ if (this.document.defaultView['hljs']) {
+ this._ready.next(this.document.defaultView['hljs']);
} else {
// Load hljs library
this._loadLibrary().pipe(
- switchMap((hljs: HighlightLibrary) => {
- if (this._options && this._options.lineNumbersLoader) {
+ switchMap((hljs: HLJSApi) => {
+ if (this.options?.lineNumbersLoader) {
// Make hljs available on window object (required for the line numbers library)
- doc.defaultView.hljs = hljs;
+ this.document.defaultView['hljs'] = hljs;
// Load line numbers library
return this.loadLineNumbers().pipe(
tap((plugin: { activateLineNumbers: () => void }) => {
@@ -38,20 +55,22 @@ export class HighlightLoader {
this._ready.next(hljs);
})
);
- } else {
+ }
+ else {
this._ready.next(hljs);
return EMPTY;
}
}),
catchError((e: any) => {
console.error('[HLJS] ', e);
+ this._ready.error(e);
return EMPTY;
})
).subscribe();
// Load highlighting theme
- if (this._options?.themePath) {
- this.loadTheme(this._options.themePath);
+ if (this.options?.themePath) {
+ this.loadTheme(this.options.themePath);
}
}
}
@@ -61,68 +80,68 @@ export class HighlightLoader {
* Lazy-Load highlight.js library
*/
private _loadLibrary(): Observable {
- 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) {
+ if (this.options.fullLibraryLoader && this.options.coreLibraryLoader) {
+ return throwError(() => LoaderErrors.FULL_WITH_CORE_LIBRARY_IMPORTS);
}
- if (this._options.fullLibraryLoader && this._options.languages) {
- return throwError(() => 'The highlighting languages were imported they are not needed!');
+ if (this.options.fullLibraryLoader && this.options.languages) {
+ return throwError(() => LoaderErrors.FULL_WITH_LANGUAGE_IMPORTS);
}
- if (this._options.coreLibraryLoader && !this._options.languages) {
- return throwError(() => 'The highlighting languages were not imported!');
+ if (this.options.coreLibraryLoader && !this.options.languages) {
+ return throwError(() => LoaderErrors.CORE_WITHOUT_LANGUAGE_IMPORTS);
}
- if (!this._options.coreLibraryLoader && this._options.languages) {
- return throwError(() => 'The core library was not imported!');
+ if (!this.options.coreLibraryLoader && this.options.languages) {
+ return throwError(() => LoaderErrors.LANGUAGE_WITHOUT_CORE_IMPORTS);
}
- if (this._options.fullLibraryLoader) {
+ 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: HighlightLibrary) => this._loadLanguages(hljs)));
+ if (this.options.coreLibraryLoader && this.options.languages && Object.keys(this.options.languages).length) {
+ return this.loadCoreLibrary().pipe(switchMap((hljs: HLJSApi) => this._loadLanguages(hljs)));
}
}
- return throwError(() => 'Highlight.js library was not imported!');
+ return throwError(() => LoaderErrors.NO_FULL_AND_NO_CORE_IMPORTS);
}
/**
* Lazy-load highlight.js languages
*/
- private _loadLanguages(hljs: HighlightLibrary): Observable {
- const languages = Object.entries(this._options.languages).map(([langName, langLoader]: [string, () => Promise]) =>
+ private _loadLanguages(hljs: HLJSApi): Observable {
+ const languages: Observable[] = Object.entries(this.options.languages).map(([langName, langLoader]: [string, () => Promise]) =>
importModule(langLoader()).pipe(
tap((langFunc: any) => hljs.registerLanguage(langName, langFunc))
)
);
- return zip(...languages).pipe(map(() => hljs));
+ return forkJoin(languages).pipe(map(() => hljs));
}
/**
* Import highlight.js core library
*/
- private loadCoreLibrary(): Observable {
- return importModule(this._options.coreLibraryLoader!());
+ private loadCoreLibrary(): Observable {
+ return importModule(this.options.coreLibraryLoader!());
}
/**
* Import highlight.js library with all languages
*/
- private loadFullLibrary(): Observable {
- return importModule(this._options.fullLibraryLoader!());
+ private loadFullLibrary(): Observable {
+ return importModule(this.options.fullLibraryLoader!());
}
/**
* Import line numbers library
*/
private loadLineNumbers(): Observable {
- return from(this._options.lineNumbersLoader!());
+ return from(this.options.lineNumbersLoader!());
}
/**
* Reload theme styles
*/
setTheme(path: string): void {
- if (isPlatformBrowser(this.platformId)) {
+ if (this.isPlatformBrowser) {
if (this._themeLinkElement) {
this._themeLinkElement.href = path;
} else {
@@ -135,12 +154,12 @@ export class HighlightLoader {
* Load theme
*/
private loadTheme(path: string): void {
- this._themeLinkElement = this.doc.createElement('link');
+ this._themeLinkElement = this.document.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);
+ this.document.head.appendChild(this._themeLinkElement);
}
}
diff --git a/projects/ngx-highlightjs/src/lib/highlight.model.ts b/projects/ngx-highlightjs/src/lib/highlight.model.ts
index 0049be1..f0e2699 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.model.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.model.ts
@@ -1,130 +1,20 @@
-import { InjectionToken } from '@angular/core';
+import { InjectionToken, Provider } from '@angular/core';
+import type { HLJSOptions } from 'highlight.js';
/**
* Full documentation is available here https://highlightjs.readthedocs.io/en/latest/api.html
*/
-export interface HighlightLibrary {
-
- /**
- * 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: string, { language, ignoreIllegals }: { language: string, ignoreIllegals: boolean }): HighlightResult;
-
- /**
- * 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: string, languageSubset: string[]): HighlightAutoResult;
-
- /**
- * 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 Element to highlight
- */
- highlightElement(element: HTMLElement): void;
-
- /**
- * 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(): void;
-
- /**
- * Configures global options:
- * @param config HighlightJs configuration argument
- */
- configure(config: HighlightConfig): void;
-
- /**
- * 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: string, languageDefinition: () => any): void;
-
- /**
- * 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: string): void;
-
- /**
- * 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: string | string[], { languageName }: { languageName: string }): void;
-
- /**
- * @return The languages names list.
- */
- listLanguages(): string[];
-
- /**
- * Looks up a language by name or alias.
- * @param name Language name
- * @return The language object if found, undefined otherwise.
- */
- getLanguage(name: string): any;
-
- /**
- * Enables safe mode. This is the default mode, providing the most reliable experience for production usage.
- */
- safeMode(): void;
-
- /**
- * Enables debug/development mode.
- */
- debugMode(): void;
-
- /**
- * Add line numbers to code element
- * @param el Code element
- */
- lineNumbersBlock(el: Element): void;
-}
-
-export interface HighlightConfig {
+export interface LineNumbersOptions {
/** classPrefix: a string prefix added before class names in the generated markup, used for backwards compatibility with stylesheets. */
- classPrefix?: string;
+ startFrom?: number;
/** languages: an array of language names and aliases restricting auto detection to only these languages. */
- languages?: string[];
- /** languageDetectRe: a regex to configure how CSS class names map to language (allows class names like say color-as-php vs the default of language-php, etc.) */
- languageDetectRe: string;
- /** noHighlightRe: a regex to configure which CSS classes are to be skipped completely. */
- noHighlightRe: string;
- /** a CSS selector to configure which elements are affected by hljs.highlightAll. Defaults to 'pre code'. */
- cssSelector: string;
-}
-
-export interface HighlightResult {
- language?: string;
- value?: string | undefined;
- relevance?: number;
- top: any;
- code: string;
- illegal: boolean;
-}
-
-export interface HighlightAutoResult {
- language?: string;
- secondBest?: any;
- value?: string | undefined;
- relevance?: number;
+ singleLine?: boolean;
}
export interface HighlightOptions {
- config?: HighlightConfig;
+ config?: Partial;
+ lineNumbersOptions?: LineNumbersOptions;
languages?: Record Promise>;
coreLibraryLoader?: () => Promise;
fullLibraryLoader?: () => Promise;
@@ -132,4 +22,13 @@ export interface HighlightOptions {
themePath?: string;
}
-export const HIGHLIGHT_OPTIONS = new InjectionToken('HIGHLIGHT_OPTIONS');
+export const HIGHLIGHT_OPTIONS: InjectionToken = new InjectionToken('HIGHLIGHT_OPTIONS');
+
+export function provideHighlightOptions(options: HighlightOptions): Provider[] {
+ return [
+ {
+ provide: HIGHLIGHT_OPTIONS,
+ useValue: options
+ }
+ ]
+}
diff --git a/projects/ngx-highlightjs/src/lib/highlight.module.ts b/projects/ngx-highlightjs/src/lib/highlight.module.ts
index 2daaf6c..7a3da9a 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.module.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.module.ts
@@ -1,9 +1,10 @@
import { NgModule } from '@angular/core';
import { Highlight } from './highlight';
+import { HighlightAuto } from './highlight-auto';
@NgModule({
- imports: [Highlight],
- exports: [Highlight]
+ imports: [Highlight, HighlightAuto],
+ exports: [Highlight, HighlightAuto]
})
export class HighlightModule {
}
diff --git a/projects/ngx-highlightjs/src/lib/highlight.service.spec.ts b/projects/ngx-highlightjs/src/lib/highlight.service.spec.ts
deleted file mode 100644
index 2340817..0000000
--- a/projects/ngx-highlightjs/src/lib/highlight.service.spec.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { TestBed, waitForAsync } from '@angular/core/testing';
-
-import { HighlightJS } from './highlight.service';
-import { BehaviorSubject } from 'rxjs';
-import * as hljs from 'highlight.js';
-import { HighlightLoader } from './highlight.loader';
-
-
-// Fake Highlight Loader
-const highlightLoaderStub = {
- ready: new BehaviorSubject(hljs)
-};
-
-describe('HighlightService', () => {
-
- let loader: HighlightLoader;
-
- beforeEach(waitForAsync(() => {
- TestBed.configureTestingModule({
- providers: [{ provide: HighlightLoader, useValue: highlightLoaderStub }]
- }).compileComponents();
- loader = TestBed.inject(HighlightLoader);
- }));
-
- it('should be created', () => {
- const service: HighlightJS = TestBed.inject(HighlightJS);
- expect(service).toBeTruthy();
- });
-});
diff --git a/projects/ngx-highlightjs/src/lib/highlight.service.ts b/projects/ngx-highlightjs/src/lib/highlight.service.ts
index 57d27e6..be44586 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.service.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.service.ts
@@ -1,34 +1,33 @@
-import { Injectable, Inject, Optional } from '@angular/core';
-import { Observable, filter, map, tap } from 'rxjs';
+import { Injectable, signal, inject, computed, WritableSignal, Signal } from '@angular/core';
+import type { HLJSApi, HighlightResult, AutoHighlightResult, LanguageFn, HLJSOptions } from 'highlight.js';
import {
- HighlightConfig,
- HighlightResult,
- HighlightLibrary,
HighlightOptions,
- HIGHLIGHT_OPTIONS,
- HighlightAutoResult
+ LineNumbersOptions,
+ HIGHLIGHT_OPTIONS
} from './highlight.model';
import { HighlightLoader } from './highlight.loader';
+
@Injectable({
providedIn: 'root'
})
export class HighlightJS {
- private _hljs: HighlightLibrary | null = null;
+ private readonly loader: HighlightLoader = inject(HighlightLoader);
- // A reference for hljs library
- get hljs(): HighlightLibrary | null {
- return this._hljs;
- }
+ private readonly options: Partial = inject(HIGHLIGHT_OPTIONS, { optional: true });
+
+ private readonly hljsSignal: WritableSignal = signal(null);
+
+ readonly hljs: Signal = computed(() => this.hljsSignal());
- constructor(private _loader: HighlightLoader, @Optional() @Inject(HIGHLIGHT_OPTIONS) options: HighlightOptions) {
+ constructor() {
// Load highlight.js library on init
- _loader.ready.subscribe((hljs: HighlightLibrary) => {
- this._hljs = hljs;
- if (options && options.config) {
+ this.loader.ready.then((hljs: HLJSApi) => {
+ this.hljsSignal.set(hljs);
+ if (this.options?.config) {
// Set global config if present
- hljs.configure(options.config);
+ hljs.configure(this.options.config);
if (hljs.listLanguages().length < 1) {
console.error('[HighlightJS]: No languages were registered!');
}
@@ -42,137 +41,115 @@ export class HighlightJS {
* @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: string, { language, ignoreIllegals }: { language: string, ignoreIllegals: boolean }): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.highlight(code, { language, ignoreIllegals }))
- );
+ async highlight(code: string, { language, ignoreIllegals }: {
+ language: string,
+ ignoreIllegals: boolean
+ }): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ return 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: string, languageSubset: string[]): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.highlightAuto(value, languageSubset))
- );
+ async highlightAuto(value: string, languageSubset: string[]): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ return 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: HTMLElement): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.highlightElement(element))
- );
+ async highlightElement(element: HTMLElement): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ 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(): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.highlightAll())
- );
+ async highlightAll(): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ hljs.highlightAll();
}
/**
* @deprecated in version 12
* Configures global options:
- * @param config HighlightJs configuration argument
*/
- configure(config: HighlightConfig): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.configure(config))
- );
+ async configure(config: Partial): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ 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: string, languageDefinition: () => any): Observable {
- return this._loader.ready.pipe(
- tap((hljs: HighlightLibrary) => hljs.registerLanguage(languageName, languageDefinition))
- );
+ async registerLanguage(languageName: string, languageDefinition: LanguageFn): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ 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.
+ * Removes a language and its aliases from the library. Used mostly internally
*/
- unregisterLanguage(languageName: string): Observable {
- return this._loader.ready.pipe(
- tap((hljs: HighlightLibrary) => hljs.unregisterLanguage(languageName))
- );
+ async unregisterLanguage(languageName: string): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ 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: string | string[], { languageName }: { languageName: string }): Observable {
- return this._loader.ready.pipe(
- tap((hljs: HighlightLibrary) => hljs.registerAliases(alias, { languageName }))
- );
+ async registerAliases(alias: string | string[], { languageName }: { languageName: string }): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ hljs.registerAliases(alias, { languageName });
}
/**
* @return The languages names list.
*/
- listLanguages(): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.listLanguages())
- );
+ async listLanguages(): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ return hljs.listLanguages();
}
/**
* Looks up a language by name or alias.
- * @param name Language name
- * @return The language object if found, undefined otherwise.
*/
- getLanguage(name: string): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.getLanguage(name))
- );
+ async getLanguage(name: string): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ return hljs.getLanguage(name);
}
/**
* Enables safe mode. This is the default mode, providing the most reliable experience for production usage.
*/
- safeMode(): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.safeMode())
- );
+ async safeMode(): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ hljs.safeMode();
}
/**
* Enables debug/development mode.
*/
- debugMode(): Observable {
- return this._loader.ready.pipe(
- map((hljs: HighlightLibrary) => hljs.debugMode())
- );
+ async debugMode(): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ hljs.debugMode();
}
/**
* Display line numbers
- * @param el Code element
*/
- lineNumbersBlock(el: HTMLElement): Observable {
- return this._loader.ready.pipe(
- filter((hljs: HighlightLibrary) => !!hljs.lineNumbersBlock),
- tap((hljs: HighlightLibrary) => hljs.lineNumbersBlock(el))
- );
+ async lineNumbersBlock(el: HTMLElement, options: LineNumbersOptions): Promise {
+ const hljs: HLJSApi = await this.loader.ready;
+ if ((hljs as any).lineNumbersBlock) {
+ (hljs as any).lineNumbersBlock(el, options);
+ }
}
}
diff --git a/projects/ngx-highlightjs/src/lib/highlight.spec.ts b/projects/ngx-highlightjs/src/lib/highlight.spec.ts
deleted file mode 100644
index 9d4cb3f..0000000
--- a/projects/ngx-highlightjs/src/lib/highlight.spec.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
-import { Component, DebugElement, Input, OnInit, PLATFORM_ID } from '@angular/core';
-import { By } from '@angular/platform-browser';
-import { BehaviorSubject } from 'rxjs';
-import hljs from 'highlight.js';
-import { Highlight } from './highlight';
-import { HighlightLoader } from './highlight.loader';
-import { HighlightLibrary } from './highlight.model';
-
-@Component({
- template: ` `,
- standalone: true,
- imports: [Highlight]
-})
-class TestHighlightComponent implements OnInit {
- @Input() code: string;
-
- ngOnInit(): void {
- }
-}
-
-// Fake Highlight Loader
-const highlightLoaderStub = {
- ready: new BehaviorSubject(hljs)
-};
-
-describe('Highlight Directive', () => {
- let component: TestHighlightComponent;
- let directiveElement: DebugElement;
- let directiveInstance: Highlight;
- let fixture: ComponentFixture;
- let loader: HighlightLoader;
- const testJsCode = 'console.log("test")';
- const testHtmlCode = '';
-
- beforeEach(waitForAsync(() => {
- TestBed.configureTestingModule({
- imports: [Highlight, TestHighlightComponent],
- providers: [
- { provide: PLATFORM_ID, useValue: 'browser' },
- { provide: HighlightLoader, useValue: highlightLoaderStub }
- ]
- }).compileComponents();
- loader = TestBed.inject(HighlightLoader);
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(TestHighlightComponent);
- component = fixture.componentInstance;
- directiveElement = fixture.debugElement.query(By.directive(Highlight));
- directiveInstance = directiveElement.injector.get(Highlight);
- fixture.detectChanges();
- });
-
- it('should create highlight directive', () => {
- expect(directiveInstance).not.toBeNull();
- });
-
- it('should add hljs class', () => {
- expect(directiveElement.nativeElement.classList.contains('hljs')).toBeTruthy();
- });
-
- it('should highlight given text', fakeAsync(() => {
- let highlightedCode: string;
- component.code = testJsCode;
- fixture.detectChanges();
- loader.ready.subscribe((lib: HighlightLibrary) => highlightedCode = lib.highlightAuto(testJsCode, null).value);
- tick(500);
- expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
- }));
-
- it('should reset text if empty string was passed', () => {
- component.code = '';
- fixture.detectChanges();
- expect(directiveElement.nativeElement.innerHTML).toBe('');
- });
-
- it('should not highlight if code is undefined', () => {
- spyOn(directiveInstance, 'highlightElement');
- component.code = null;
- fixture.detectChanges();
- expect(directiveInstance.highlightElement).not.toHaveBeenCalled();
- });
-
- it('should highlight given text and highlight another text when change', fakeAsync(() => {
- let highlightedCode: string;
- component.code = testJsCode;
- fixture.detectChanges();
- loader.ready.subscribe((lib: HighlightLibrary) => highlightedCode = lib.highlightAuto(testJsCode, null).value);
- tick(500);
- expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
-
- // Change code 2nd time with another value
- component.code = testHtmlCode;
- fixture.detectChanges();
- loader.ready.subscribe((lib: HighlightLibrary) => highlightedCode = lib.highlightAuto(testHtmlCode, null).value);
- tick(500);
- expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
-
- // Change code 3rd time but with empty string
- component.code = '';
- fixture.detectChanges();
- tick(300);
- expect(directiveElement.nativeElement.innerHTML).toBe('');
-
- // Change code 4th time but with nullish value
- component.code = null;
- fixture.detectChanges();
- tick(300);
- expect(directiveElement.nativeElement.innerHTML).toBe('');
- }));
-});
diff --git a/projects/ngx-highlightjs/src/lib/highlight.ts b/projects/ngx-highlightjs/src/lib/highlight.ts
index 222c1ba..45ea6de 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.ts
@@ -2,131 +2,48 @@ import {
Directive,
Input,
Output,
- Inject,
- Optional,
+ signal,
+ booleanAttribute,
+ input,
EventEmitter,
- PLATFORM_ID,
- OnChanges,
- SimpleChanges,
- ElementRef,
- SecurityContext
+ InputSignal,
+ WritableSignal
} from '@angular/core';
-import { isPlatformBrowser } from '@angular/common';
-import { DomSanitizer } from '@angular/platform-browser';
-import { animationFrameScheduler } from 'rxjs';
-import { HighlightJS } from './highlight.service';
-import { HIGHLIGHT_OPTIONS, HighlightOptions, HighlightAutoResult } from './highlight.model';
-import { trustedHTMLFromStringBypass } from './trusted-types';
+import type { HighlightResult } from 'highlight.js';
+import { HighlightBase } from './highlight-base';
@Directive({
+ standalone: true,
+ selector: '[highlight]',
+ providers: [{ provide: HighlightBase, useExisting: Highlight }],
host: {
'[class.hljs]': 'true'
- },
- selector: '[highlight]',
- standalone: true
+ }
})
-export class Highlight implements OnChanges {
+export class Highlight extends HighlightBase {
- // Highlighted Code
- private readonly _nativeElement: HTMLElement;
+ // Code to highlight
+ code: InputSignal = input(null, { alias: 'highlight' });
- // Temp observer to observe when line numbers has been added to code element
- private _lineNumbersObs: any;
-
- // Highlight code input
- @Input('highlight') code: string | null;
+ // Highlighted result
+ highlightResult: WritableSignal = signal(null);
// 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.
- @Input() languages!: string[];
+ @Input({ required: true }) language: string;
- // Show line numbers
- @Input() lineNumbers!: boolean;
+ // An optional flag, when set to true it forces highlighting to finish even in case of detecting
+ // illegal syntax for the language instead of throwing an exception.
+ @Input({ transform: booleanAttribute }) ignoreIllegals!: boolean;
// Stream that emits when code string is highlighted
- @Output() highlighted = new EventEmitter();
-
- constructor(el: ElementRef,
- private _hljs: HighlightJS,
- private _sanitizer: DomSanitizer,
- @Inject(PLATFORM_ID) private platformId: object,
- @Optional() @Inject(HIGHLIGHT_OPTIONS) private _options: HighlightOptions) {
- this._nativeElement = el.nativeElement;
- }
-
- ngOnChanges(changes: SimpleChanges) {
- if (
- isPlatformBrowser(this.platformId) &&
- changes?.code?.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: string, languages: string[]): void {
- // Set code text before highlighting
- this.setTextContent(code);
- this._hljs.highlightAuto(code, languages).subscribe((res: HighlightAutoResult) => {
- // Set highlighted code
- this.setInnerHTML(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);
- });
- }
+ @Output() highlighted: EventEmitter = new EventEmitter();
- private 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 });
+ async highlightElement(code: string): Promise {
+ const res: HighlightResult = await this._hljs.highlight(code, {
+ language: this.language,
+ ignoreIllegals: this.ignoreIllegals
});
- }
-
- private destroyLineNumbersObserver() {
- if (this._lineNumbersObs) {
- this._lineNumbersObs.disconnect();
- this._lineNumbersObs = null;
- }
- }
-
- private setTextContent(content: string) {
- animationFrameScheduler.schedule(() =>
- this._nativeElement.textContent = content
- );
- }
-
- private setInnerHTML(content: string | null) {
- animationFrameScheduler.schedule(() =>
- this._nativeElement.innerHTML = trustedHTMLFromStringBypass(
- this._sanitizer.sanitize(SecurityContext.HTML, content) || ''
- )
- );
+ this.highlightResult.set(res);
}
}
-
diff --git a/projects/ngx-highlightjs/src/lib/loader-errors.ts b/projects/ngx-highlightjs/src/lib/loader-errors.ts
new file mode 100644
index 0000000..a20e26f
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/loader-errors.ts
@@ -0,0 +1,7 @@
+export enum LoaderErrors {
+ FULL_WITH_CORE_LIBRARY_IMPORTS = 'The full library and the core library were imported, only one of them should be imported!',
+ FULL_WITH_LANGUAGE_IMPORTS = 'The highlighting languages were imported they are not needed!',
+ CORE_WITHOUT_LANGUAGE_IMPORTS = 'The highlighting languages were not imported!',
+ LANGUAGE_WITHOUT_CORE_IMPORTS = 'The core library was not imported!',
+ NO_FULL_AND_NO_CORE_IMPORTS = 'Highlight.js library was not imported!',
+}
diff --git a/projects/ngx-highlightjs/src/lib/tests/common-tests.ts b/projects/ngx-highlightjs/src/lib/tests/common-tests.ts
new file mode 100644
index 0000000..adb957b
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/common-tests.ts
@@ -0,0 +1,16 @@
+import hljs, { type HLJSApi } from 'highlight.js';
+import { activateLineNumbers } from 'ngx-highlightjs/line-numbers';
+
+export async function afterTimeout(timeout: number): Promise {
+ // Use await with a setTimeout promise
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+}
+
+// Fake Highlight Loader
+export const highlightLoaderStub = {
+ ready: new Promise((resolve) => {
+ document.defaultView['hljs'] = hljs;
+ activateLineNumbers();
+ resolve(document.defaultView['hljs']);
+ })
+};
diff --git a/projects/ngx-highlightjs/src/lib/tests/highlight-auto.spec.ts b/projects/ngx-highlightjs/src/lib/tests/highlight-auto.spec.ts
new file mode 100644
index 0000000..2c12c9d
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight-auto.spec.ts
@@ -0,0 +1,87 @@
+import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
+import { Component, Input, DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { HighlightAuto, HighlightLoader } from 'ngx-highlightjs';
+import hljs from 'highlight.js';
+import { afterTimeout, highlightLoaderStub } from './common-tests';
+
+@Component({
+ template: ` `,
+ standalone: true,
+ imports: [HighlightAuto]
+})
+class TestHighlightComponent {
+ @Input() code: string;
+}
+
+describe('HighlightAuto Directive', () => {
+ let component: TestHighlightComponent;
+ let directiveElement: DebugElement;
+ let directiveInstance: HighlightAuto;
+ let fixture: ComponentFixture;
+
+ const testJsCode: string = 'console.log("test")';
+ const testHtmlCode: string = '';
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [HighlightAuto, TestHighlightComponent],
+ providers: [
+ { provide: HighlightLoader, useValue: highlightLoaderStub },
+ { provide: ComponentFixtureAutoDetect, useValue: true }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestHighlightComponent);
+ component = fixture.componentInstance;
+ directiveElement = fixture.debugElement.query(By.directive(HighlightAuto));
+ directiveInstance = directiveElement.injector.get(HighlightAuto);
+ });
+
+ it('should create highlight directive', () => {
+ expect(directiveInstance).toBeTruthy();
+ });
+
+ it('should add hljs class', () => {
+ expect(directiveElement.nativeElement.classList.contains('hljs')).toBeTruthy();
+ });
+
+ it('should reset text if empty string was passed', () => {
+ component.code = '';
+ fixture.detectChanges();
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+ });
+
+ it('should highlight given text and highlight another text when change', async () => {
+ component.code = testJsCode;
+ fixture.detectChanges()
+
+ let highlightedCode: string = hljs.highlightAuto(testJsCode, null).value;
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
+
+ // Change code 2nd time with another value
+ component.code = testHtmlCode;
+ fixture.detectChanges();
+
+ highlightedCode = hljs.highlightAuto(testHtmlCode, null).value;
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
+
+ // Change code 3rd time but with empty string
+ component.code = '';
+ fixture.detectChanges();
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+
+ // Change code 4th time but with nullish value
+ component.code = null;
+ fixture.detectChanges();
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+ });
+});
diff --git a/projects/ngx-highlightjs/src/lib/tests/highlight.loader.spec.ts b/projects/ngx-highlightjs/src/lib/tests/highlight.loader.spec.ts
new file mode 100644
index 0000000..f395c67
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.loader.spec.ts
@@ -0,0 +1,174 @@
+import { TestBed } from '@angular/core/testing';
+import { HIGHLIGHT_OPTIONS, HighlightLoader, HighlightOptions } from 'ngx-highlightjs';
+import hljs, { HLJSApi } from 'highlight.js';
+import { LoaderErrors } from '../loader-errors';
+// import * as lineNumbersModule from 'ngx-highlightjs/line-numbers';
+
+
+const fullLibraryLoader = () => import('highlight.js');
+// const lineNumbersLoader = () => import('ngx-highlightjs/line-numbers');
+const coreLibraryLoader = () => import('highlight.js/lib/core');
+const typescript = () => import('highlight.js/lib/languages/typescript');
+
+describe('HighlightService', () => {
+
+ // const loader: HighlightLoader;
+
+ beforeEach(() => {
+ // Clean up hljs
+ document.defaultView['hljs'] = null;
+ // await TestBed.configureTestingModule({
+ // providers: [{ provide: HighlightLoader, useValue: highlightLoaderStub }]
+ // }).compileComponents();
+
+ // loader = TestBed.inject(HighlightLoader);
+ });
+
+ // it('should load the library', async () => {
+ // const lib: HLJSApi = await loader.ready;
+ // expect(lib).toBeTruthy();
+ // });
+
+ it('should work when library is loaded externally', async () => {
+ document.defaultView['hljs'] = hljs;
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ const lib: HLJSApi = await loader.ready;
+ expect(lib).toBe(hljs);
+ });
+
+ it('should throw an error if library options did not exist', async () => {
+ try {
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ await loader.ready;
+ } catch (error) {
+ expect(error).toBe(LoaderErrors.NO_FULL_AND_NO_CORE_IMPORTS);
+ }
+ });
+
+ it('should throw an error if both fullLibrary and coreLibrary loaders were provided', async () => {
+ try {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ fullLibraryLoader: fullLibraryLoader,
+ coreLibraryLoader: coreLibraryLoader
+ } as HighlightOptions
+ });
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ await loader.ready;
+ } catch (error) {
+ expect(error).toBe(LoaderErrors.FULL_WITH_CORE_LIBRARY_IMPORTS);
+ }
+ });
+
+ it('should throw an error if both fullLibrary and languages loaders were provided', async () => {
+ try {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ fullLibraryLoader: fullLibraryLoader,
+ languages: {
+ typescript: typescript
+ }
+ } as HighlightOptions
+ });
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ await loader.ready;
+ } catch (error) {
+ expect(error).toBe(LoaderErrors.FULL_WITH_LANGUAGE_IMPORTS);
+ }
+ });
+
+ it('should throw an error if coreLibrary was provided without any language', async () => {
+ try {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ coreLibraryLoader: coreLibraryLoader
+ } as HighlightOptions
+ });
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ await loader.ready;
+ } catch (error) {
+ expect(error).toBe(LoaderErrors.CORE_WITHOUT_LANGUAGE_IMPORTS);
+ }
+ });
+
+ it('should throw an error if languages were provided without the coreLibrary', async () => {
+ try {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ languages: {
+ typescript: typescript
+ }
+ } as HighlightOptions
+ });
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ await loader.ready;
+ } catch (error) {
+ expect(error).toBe(LoaderErrors.LANGUAGE_WITHOUT_CORE_IMPORTS);
+ }
+ });
+
+
+ it('should create style element when loading a theme', () => {
+ document.defaultView['hljs'] = hljs;
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+
+ const path: string = 'https://path-to-theme.css/';
+
+ const linkElement: HTMLLinkElement = document.createElement('link');
+ const createElementSpy: jasmine.Spy = spyOn(document, 'createElement').and.returnValue(linkElement);
+ const appendChildSpy: jasmine.Spy = spyOn(document.head, 'appendChild');
+
+ (loader as any).loadTheme(path);
+
+ expect(createElementSpy).toHaveBeenCalledWith('link');
+ expect(loader['_themeLinkElement']).toBeTruthy();
+ expect(loader['_themeLinkElement'].href).toBe(path);
+ expect(loader['_themeLinkElement'].type).toBe('text/css');
+ expect(loader['_themeLinkElement'].rel).toBe('stylesheet');
+ expect(loader['_themeLinkElement'].media).toBe('screen,print');
+ expect(appendChildSpy).toHaveBeenCalledWith(loader['_themeLinkElement']);
+ });
+
+ it('should update existing style element when setting a theme', () => {
+ document.defaultView['hljs'] = hljs;
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+
+ const path: string = 'https://diff-theme-path.css/';
+ const existingElement: HTMLLinkElement = document.createElement('link');
+ existingElement.href = 'https://initial-theme-path.css/';
+ spyOn(document, 'createElement').and.returnValue(existingElement);
+
+ loader.setTheme(path);
+
+ expect(existingElement.href).toBe(path);
+ });
+
+ it('should load a new style element when setting a theme if no existing element', () => {
+ document.defaultView['hljs'] = hljs;
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+
+ const path: string = 'https://path-to-theme.css/';
+ spyOn(loader as any, 'loadTheme');
+
+ loader.setTheme(path);
+
+ expect((loader as any).loadTheme).toHaveBeenCalledWith(path);
+ });
+
+ // fit('should load line numbers library if provided in the options', () => {
+ // document.defaultView['hljs'] = hljs;
+ //
+ // const lineNumberActivateSpy: jasmine.Spy = spyOn(lineNumbersModule, 'activateLineNumbers');
+ //
+ // TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ // useValue: {
+ // lineNumbersLoader
+ // } as HighlightOptions
+ // });
+ //
+ // const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ //
+ // expect(lineNumberActivateSpy).toHaveBeenCalled();
+ // });
+
+});
diff --git a/projects/ngx-highlightjs/src/lib/tests/highlight.service.spec.ts b/projects/ngx-highlightjs/src/lib/tests/highlight.service.spec.ts
new file mode 100644
index 0000000..ba7b45b
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.service.spec.ts
@@ -0,0 +1,173 @@
+import { TestBed } from '@angular/core/testing';
+import { HIGHLIGHT_OPTIONS, HighlightJS, HighlightLoader, HighlightOptions } from 'ngx-highlightjs';
+import hljs from 'highlight.js';
+import { highlightLoaderStub } from './common-tests';
+
+import md from 'highlight.js/lib/languages/markdown';
+
+
+describe('HighlightService', () => {
+
+ const testJsCode: string = 'console.log("test")';
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ providers: [
+ { provide: HighlightLoader, useValue: highlightLoaderStub }
+ ]
+ }).compileComponents();
+ });
+
+ it('should be created', () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ expect(service).toBeTruthy();
+ });
+
+ it('should override the default config of highlight.js', async () => {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ config: {
+ languages: ['ts', 'html']
+ }
+ } as HighlightOptions
+ });
+ const configureSpy: jasmine.Spy = spyOn(hljs,'configure');
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ TestBed.inject(HighlightJS);
+ await loader.ready;
+ expect(configureSpy).toHaveBeenCalledWith({
+ languages: ['ts', 'html']
+ });
+ });
+
+ it('should call hljs [highlight] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const highlightSpy: jasmine.Spy = spyOn(hljs, 'highlight');
+
+ await service.highlight(testJsCode, {
+ language: 'ts',
+ ignoreIllegals: false
+ });
+
+ expect(highlightSpy).toHaveBeenCalledWith(testJsCode, {
+ language: 'ts',
+ ignoreIllegals: false
+ });
+ });
+
+ it('should call hljs [highlightAuto] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const highlightAutoSpy: jasmine.Spy = spyOn(hljs, 'highlightAuto');
+
+ await service.highlightAuto(testJsCode, ['ts', 'html']);
+
+ expect(highlightAutoSpy).toHaveBeenCalledWith(testJsCode, ['ts', 'html']);
+ });
+
+ it('should call hljs [highlightElement] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const element: HTMLElement = document.createElement('div');
+ element.innerHTML = testJsCode;
+
+ const highlightElementSpy: jasmine.Spy = spyOn(hljs, 'highlightElement');
+
+ await service.highlightElement(element);
+
+ expect(highlightElementSpy).toHaveBeenCalledWith(element);
+ });
+
+
+ it('should call hljs [highlightAll] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const highlightAllSpy: jasmine.Spy = spyOn(hljs, 'highlightAll');
+
+ await service.highlightAll();
+
+ expect(highlightAllSpy).toHaveBeenCalled();
+ });
+
+ it('should call hljs [highlightAll] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const configureSpy: jasmine.Spy = spyOn(hljs, 'configure');
+
+ await service.configure({ languages: ['ts', 'html'] });
+
+ expect(configureSpy).toHaveBeenCalledWith({ languages: ['ts', 'html'] });
+ });
+
+ it('should call hljs [registerLanguage] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const registerLanguageSpy: jasmine.Spy = spyOn(hljs, 'registerLanguage');
+
+ await service.registerLanguage('markdown', md);
+
+ expect(registerLanguageSpy).toHaveBeenCalledWith('markdown', md);
+ });
+
+
+ it('should call hljs [debugMode] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const debugModeSpy: jasmine.Spy = spyOn(hljs, 'debugMode');
+
+ await service.debugMode();
+
+ expect(debugModeSpy).toHaveBeenCalled();
+ });
+
+ it('should call hljs [safeMode] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const safeModeSpy: jasmine.Spy = spyOn(hljs, 'safeMode');
+
+ await service.safeMode();
+
+ expect(safeModeSpy).toHaveBeenCalled();
+ });
+
+ it('should call hljs [getLanguage] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const getLanguageSpy: jasmine.Spy = spyOn(hljs, 'getLanguage');
+
+ await service.getLanguage('html');
+
+ expect(getLanguageSpy).toHaveBeenCalledWith('html');
+ });
+
+ it('should call hljs [listLanguages] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const listLanguagesSpy: jasmine.Spy = spyOn(hljs, 'listLanguages');
+
+ await service.listLanguages();
+
+ expect(listLanguagesSpy).toHaveBeenCalled();
+ });
+
+ it('should call hljs [unregisterLanguage] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const unregisterLanguageSpy: jasmine.Spy = spyOn(hljs, 'unregisterLanguage');
+
+ await service.unregisterLanguage('markdown');
+
+ expect(unregisterLanguageSpy).toHaveBeenCalledWith('markdown');
+ });
+
+ it('should call hljs [registerAliases] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const registerAliasesSpy: jasmine.Spy = spyOn(hljs, 'registerAliases');
+
+ await service.registerAliases('md', { languageName: 'markdown' });
+
+ expect(registerAliasesSpy).toHaveBeenCalledWith('md', { languageName: 'markdown' });
+ });
+
+
+ it('should call hljs [lineNumbersBlock] function', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ const element: HTMLElement = document.createElement('div');
+ element.innerHTML = testJsCode;
+ const registerAliasesSpy: jasmine.Spy = spyOn(hljs as any, 'lineNumbersBlock');
+
+ await service.lineNumbersBlock(element, { singleLine: true });
+
+ expect(registerAliasesSpy).toHaveBeenCalledWith(element, { singleLine: true });
+ });
+});
diff --git a/projects/ngx-highlightjs/src/lib/tests/highlight.spec.ts b/projects/ngx-highlightjs/src/lib/tests/highlight.spec.ts
new file mode 100644
index 0000000..caf7064
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.spec.ts
@@ -0,0 +1,96 @@
+import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
+import { Component, Input, DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { Highlight, HighlightLoader } from 'ngx-highlightjs';
+import hljs from 'highlight.js';
+import { afterTimeout, highlightLoaderStub } from './common-tests';
+
+@Component({
+ template: ` `,
+ standalone: true,
+ imports: [Highlight]
+})
+class TestHighlightComponent {
+ @Input() code: string;
+ @Input() language: string;
+}
+
+describe('Highlight Directive', () => {
+ let component: TestHighlightComponent;
+ let directiveElement: DebugElement;
+ let directiveInstance: Highlight;
+ let fixture: ComponentFixture;
+
+ const testJsCode: string = 'console.log("test")';
+ const testHtmlCode: string = '';
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Highlight, TestHighlightComponent],
+ providers: [
+ { provide: HighlightLoader, useValue: highlightLoaderStub },
+ { provide: ComponentFixtureAutoDetect, useValue: true }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestHighlightComponent);
+ component = fixture.componentInstance;
+ directiveElement = fixture.debugElement.query(By.directive(Highlight));
+ directiveInstance = directiveElement.injector.get(Highlight);
+ });
+
+ it('should create highlight directive', () => {
+ expect(directiveInstance).toBeTruthy();
+ });
+
+ it('should add hljs class', () => {
+ expect(directiveElement.nativeElement.classList.contains('hljs')).toBeTruthy();
+ });
+
+ it('should reset text if empty string was passed', () => {
+ component.code = '';
+ fixture.detectChanges();
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+ });
+
+ it('should highlight code reactively', async () => {
+ component.language = 'ts';
+ component.code = testJsCode;
+ fixture.detectChanges();
+
+ let highlightedCode: string = hljs.highlight(testJsCode, {
+ language: component.language,
+ ignoreIllegals: false
+ }).value;
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
+
+ // Change code 2nd time with another value
+ component.language = 'html';
+ component.code = testHtmlCode;
+ fixture.detectChanges();
+
+ highlightedCode = hljs.highlight(testHtmlCode, {
+ language: component.language,
+ ignoreIllegals: false
+ }).value;
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
+
+ // Change code 3rd time but with empty string
+ component.code = '';
+ fixture.detectChanges();
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+
+ // Change code 4th time but with nullish value
+ component.code = null;
+ fixture.detectChanges();
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe('');
+ });
+});
diff --git a/projects/ngx-highlightjs/src/public-api.ts b/projects/ngx-highlightjs/src/public-api.ts
index c3961f2..1713fb4 100644
--- a/projects/ngx-highlightjs/src/public-api.ts
+++ b/projects/ngx-highlightjs/src/public-api.ts
@@ -1,4 +1,6 @@
+export * from './lib/highlight-base';
export * from './lib/highlight';
+export * from './lib/highlight-auto';
export * from './lib/highlight.model';
export * from './lib/highlight.module';
export * from './lib/highlight.service';
diff --git a/projects/ngx-highlightjs/src/test.ts b/projects/ngx-highlightjs/src/test.ts
deleted file mode 100644
index 70b0070..0000000
--- a/projects/ngx-highlightjs/src/test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
-
-import 'zone.js';
-import 'zone.js/testing';
-import { getTestBed } from '@angular/core/testing';
-import {
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
-
-// First, initialize the Angular testing environment.
-getTestBed().initTestEnvironment(
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting(), {
- teardown: { destroyAfterEach: false }
-}
-);
diff --git a/projects/ngx-highlightjs/tsconfig.lib.json b/projects/ngx-highlightjs/tsconfig.lib.json
index f98c169..543fd47 100644
--- a/projects/ngx-highlightjs/tsconfig.lib.json
+++ b/projects/ngx-highlightjs/tsconfig.lib.json
@@ -1,18 +1,14 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
- "declarationMap": true,
"declaration": true,
+ "declarationMap": true,
"inlineSources": true,
- "types": [],
- "lib": [
- "dom",
- "es2018"
- ]
+ "types": []
},
"exclude": [
- "src/test.ts",
"**/*.spec.ts"
]
}
diff --git a/projects/ngx-highlightjs/tsconfig.lib.prod.json b/projects/ngx-highlightjs/tsconfig.lib.prod.json
index 2a2faa8..06de549 100644
--- a/projects/ngx-highlightjs/tsconfig.lib.prod.json
+++ b/projects/ngx-highlightjs/tsconfig.lib.prod.json
@@ -1,3 +1,4 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
diff --git a/projects/ngx-highlightjs/tsconfig.spec.json b/projects/ngx-highlightjs/tsconfig.spec.json
index 16da33d..ce7048b 100644
--- a/projects/ngx-highlightjs/tsconfig.spec.json
+++ b/projects/ngx-highlightjs/tsconfig.spec.json
@@ -1,15 +1,12 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
- "jasmine",
- "node"
+ "jasmine"
]
},
- "files": [
- "src/test.ts"
- ],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
diff --git a/tsconfig.app.json b/tsconfig.app.json
deleted file mode 100644
index f758d98..0000000
--- a/tsconfig.app.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "outDir": "./out-tsc/app",
- "types": []
- },
- "files": [
- "src/main.ts",
- "src/polyfills.ts"
- ],
- "include": [
- "src/**/*.d.ts"
- ]
-}
diff --git a/tsconfig.json b/tsconfig.json
index 2e87f47..bc985e8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,38 +1,43 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
- "baseUrl": "./",
"outDir": "./dist/out-tsc",
- "sourceMap": true,
- "declaration": false,
- "downlevelIteration": true,
- "experimentalDecorators": true,
- "module": "es2020",
- "moduleResolution": "node",
- "importHelpers": true,
- "target": "ES2022",
- "typeRoots": [
- "node_modules/@types"
- ],
- "lib": [
- "es2018",
- "dom"
- ],
+ "strict": false,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
"paths": {
"ngx-highlightjs": [
- "projects/ngx-highlightjs/src/public-api"
+ "./projects/ngx-highlightjs/src/public-api"
],
"ngx-highlightjs/plus": [
- "projects/ngx-highlightjs/plus/src/public_api"
+ "./projects/ngx-highlightjs/plus/src/public_api"
],
"ngx-highlightjs/line-numbers": [
- "projects/ngx-highlightjs/line-numbers/src/public_api"
+ "./projects/ngx-highlightjs/line-numbers/src/public_api"
]
},
- "useDefineForClassFields": false
+ "esModuleInterop": true,
+ "sourceMap": true,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "useDefineForClassFields": false,
+ "lib": [
+ "ES2022",
+ "dom"
+ ]
},
"angularCompilerOptions": {
- "fullTemplateTypeCheck": true,
- "strictInjectionParameters": true
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
}
}
diff --git a/tsconfig.spec.json b/tsconfig.spec.json
deleted file mode 100644
index 6400fde..0000000
--- a/tsconfig.spec.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "outDir": "./out-tsc/spec",
- "types": [
- "jasmine",
- "node"
- ]
- },
- "files": [
- "src/test.ts",
- "src/polyfills.ts"
- ],
- "include": [
- "src/**/*.spec.ts",
- "src/**/*.d.ts"
- ]
-}
| |