diff --git a/projects/ngx-highlightjs-demo/src/assets/example-code.ts b/projects/ngx-highlightjs-demo/src/assets/example-code.ts
new file mode 100644
index 0000000..252c6d2
--- /dev/null
+++ b/projects/ngx-highlightjs-demo/src/assets/example-code.ts
@@ -0,0 +1 @@
+console.log('HELLO!')
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/README.md b/projects/ngx-highlightjs/README.md
index 7f69093..9176737 100644
--- a/projects/ngx-highlightjs/README.md
+++ b/projects/ngx-highlightjs/README.md
@@ -7,12 +7,13 @@
[![Stackblitz](https://img.shields.io/badge/stackblitz-online-orange.svg)](https://stackblitz.com/edit/ngx-highlightjs)
[![npm](https://img.shields.io/npm/v/ngx-highlightjs.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/ngx-highlightjs)
[![tests](https://github.com/MurhafSousli/ngx-highlightjs/workflows/tests/badge.svg)](https://github.com/MurhafSousli/ngx-highlightjs/actions?query=workflow%3Atests)
+[![codecov](https://codecov.io/gh/MurhafSousli/ngx-highlightjs/graph/badge.svg?token=JWAelEiLT1)](https://codecov.io/gh/MurhafSousli/ngx-highlightjs)
[![Downloads](https://img.shields.io/npm/dt/ngx-highlightjs.svg?maxAge=2592000?style=plastic)](https://www.npmjs.com/package/ngx-highlightjs)
[![Monthly Downloads](https://img.shields.io/npm/dm/ngx-highlightjs.svg)](https://www.npmjs.com/package/ngx-highlightjs)
[![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/ngx-highlightjs.svg)](https://bundlephobia.com/result?p=ngx-highlightjs)
[![License](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](/LICENSE)
-Instant code highlighting, auto-detect language, super easy to use
+Instant code highlighting directives
___
## Table of Contents
@@ -20,7 +21,8 @@ ___
- [Live Demo](https://ngx-highlight.netlify.com/) | [Stackblitz](https://stackblitz.com/edit/ngx-highlightjs)
- [Installation](#installation)
- [Usage](#usage)
-- [Development](#development)
+- [Line numbers addon usage](#line-numbers)
+- [Plus package](#plus)
- [Issues](#issues)
- [Author](#author)
- [More plugins](#more-plugins)
@@ -39,75 +41,62 @@ npm i ngx-highlightjs
## Usage
-### Provide the config for `HIGHLIGHT_OPTIONS` in `main.ts` file
+### Use `provideHighlightOptions` to provide highlight.js options in `app.config.ts`
```typescript
-import { HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
+import { provideHighlightOptions } from 'ngx-highlightjs';
-bootstrapApplication(AppComponent, {
+export const appConfig: ApplicationConfig = {
providers: [
- {
- provide: HIGHLIGHT_OPTIONS,
- useValue: {
- fullLibraryLoader: () => import('highlight.js')
- }
- }
+ provideHighlightOptions({
+ fullLibraryLoader: () => import('highlight.js')
+ })
]
-})
+};
```
-> Note: This will add highlight.js library including all languages to your bundle.
+> Note: This includes the entire Highlight.js library with all languages.
-To avoid import everything from highlight.js library, you should import each language you want to highlight manually.
+You can also opt to load only the core script and the necessary languages.
-### Import only the core library and the needed highlighting languages
+### Importing Core Library and Languages
```typescript
-import { HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
+import { provideHighlightOptions } from 'ngx-highlightjs';
-bootstrapApplication(AppComponent, {
+export const appConfig: ApplicationConfig = {
providers: [
- {
- provide: HIGHLIGHT_OPTIONS,
- useValue: {
- coreLibraryLoader: () => import('highlight.js/lib/core'),
- lineNumbersLoader: () => import('ngx-highlightjs/line-numbers'), // Optional, only if you want the line numbers
- languages: {
- typescript: () => import('highlight.js/lib/languages/typescript'),
- css: () => import('highlight.js/lib/languages/css'),
- xml: () => import('highlight.js/lib/languages/xml')
- },
- themePath: 'path-to-theme.css' // Optional, and useful if you want to change the theme dynamically
- }
- }
+ provideHighlightOptions({
+ coreLibraryLoader: () => import('highlight.js/lib/core'),
+ lineNumbersLoader: () => import('ngx-highlightjs/line-numbers'), // Optional, add line numbers if needed
+ languages: {
+ typescript: () => import('highlight.js/lib/languages/typescript'),
+ css: () => import('highlight.js/lib/languages/css'),
+ xml: () => import('highlight.js/lib/languages/xml')
+ },
+ themePath: 'path-to-theme.css' // Optional, useful for dynamic theme changes
+ })
]
-})
+};
```
### HighlightOptions API
-| Name | Description |
-|-------------------|-------------------------------------------------------------------------------------------------------------------------|
-| fullLibraryLoader | A function that returns a promise that loads `highlight.js` full script |
-| coreLibraryLoader | A function that returns a promise that loads `highlight.js` core script |
-| lineNumbersLoader | A function that returns a promise that loads `line-numbers` script which adds line numbers to the highlight code |
-| languages | The set of languages to register |
-| config | Set highlight.js config, see [configure-options](http://highlightjs.readthedocs.io/en/latest/api.html#configure-option) |
-| themePath | The path to highlighting theme CSS file |
-
-> **NOTE:** Since the update of highlight.js@v10.x.x, should
-> use `coreLibraryLoader: () => import('highlight.js/lib/core')` instead
-> of `coreLibraryLoader: () => import('highlight.js/lib/highlight')`
+| Name | Description |
+|--------------------|--------------------------------------------------------------------------------------------------------------------------------|
+| fullLibraryLoader | A function returning a promise to load the entire `highlight.js` script |
+| coreLibraryLoader | A function returning a promise to load the core `highlight.js` script |
+| lineNumbersLoader | A function returning a promise to load the `lineNumbers` script for adding line numbers |
+| languages | The languages to register with Highlight.js (Needed only if you opt to use `coreLibraryLoader`) |
+| config | Set Highlight.js configuration, see [configure-options](http://highlightjs.readthedocs.io/en/latest/api.html#configure-option) |
+| lineNumbersOptions | Set line numbers plugin options |
+| themePath | The path to the CSS file for the highlighting theme |
### Import highlighting theme
-**In version >=6.1.0**, A new way is available to load the theme dynamically! this is **OPTIONAL**, you can still use
-the traditional way.
-
-**Dynamic way**
+**Dynamic Approach**
-Set the theme path in the global config, this makes it possible to change the theme on the fly, which is useful if you
-have light and dark theme in your app.
+Set the theme path in the global configuration to enable dynamic theme changes:
```ts
providers: [
@@ -121,32 +110,26 @@ have light and dark theme in your app.
]
```
-If you want to import it from the app dist folder, then copy the themes you want to your `assets` directory, or you can
-just use a CDN link to the theme.
+Alternatively, import the theme from the app's distribution folder or use a CDN link.
-When switching between the app themes you need to call the `setTheme(path)` from the `HighlightLoader` service.
+When switching between app themes, call the `setTheme(path)` method from the `HighlightLoader` service.
```ts
import { HighlightLoader } from 'ngx-highlightjs';
export class AppComponent {
- constructor(private hljsLoader: HighlightLoader) {
- }
+ private hljsLoader: HighlightLoader = inject(HighlightLoader);
- // Assume you have a callback function when your app theme is changed
onAppThemeChange(appTheme: 'dark' | 'light') {
this.hljsLoader.setTheme(appTheme === 'dark' ? 'assets/styles/solarized-dark.css' : 'assets/styles/solarized-light.css');
}
}
```
-> You can still use the traditional way
-
+**Traditional Approach**
-**Traditional way**
-
-To import highlight.js theme from the node_modules directory in `angular.json`
+In `angular.json`:
```
"styles": [
@@ -155,7 +138,7 @@ To import highlight.js theme from the node_modules directory in `angular.json`
]
```
-Or directly in `src/style.scss`
+Or directly in `src/style.scss`:
```css
@import '~highlight.js/styles/github.css';
@@ -165,42 +148,94 @@ _[List of all available themes from highlight.js](https://github.com/isagalaev/h
### Use highlight directive
-The following line will highlight the given code and append it to the host element
+To apply code highlighting, use the `highlight` directive. It requires setting the target language, with an optional feature to ignore illegal syntax.
+
+```ts
+import { Highlight } from 'ngx-highlightjs';
+
+@Component({
+ standalone: true,
+ selector: 'app-root',
+ template: `
+
+ `,
+ imports: [Highlight]
+})
+export class AppComponent {
+}
+```
+
+## Options
+
+| Name | Type | Description |
+|----------------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **[highlight]** | string | Code to highlight. |
+| **[language]** | string | Parameter must be present and specify the language name or alias of the grammar to be used for highlighting. |
+| **[ignoreIllegals]** | boolean | An optional parameter that when true forces highlighting to finish even in case of detecting illegal syntax for the language instead of throwing an exception. |
+| **(highlighted)** | HighlightResult | Stream that emits the result object when element is highlighted |
+
+### `highlightAuto` directive
+
+The `highlightAuto` directive works the same way but automatically detects the language to apply highlighting.
```ts
-import { HighlightModule } from 'ngx-highlightjs';
+import { HighlightAuto } from 'ngx-highlightjs';
@Component({
selector: 'app-root',
template: `
-
`,
standalone: true,
- imports: [
- HighlightModule
- ]
+ imports: [HighlightAuto]
})
export class AppComponent {
}
```
-[Demo stackblitz](https://stackblitz.com/edit/ngx-highlightjs)
+## Options
+
+| Name | Type | Description |
+|---------------------|---------------------|------------------------------------------------------------------------------------------------------------|
+| **[highlightAuto]** | string | Accept code string to highlight, default `null` |
+| **[languages]** | string[] | An array of language names and aliases restricting auto detection to only these languages, default: `null` |
+| **(highlighted)** | AutoHighlightResult | Stream that emits the result object when element is highlighted |
+
+
+
+
+### `lineNumbers` directive
+
+The `lineNumbers` directive extends highlighted code with line numbers. It functions in conjunction with the `highlight` and `highlightAuto` directives.
+
+```ts
+import { HighlightAuto } from 'ngx-highlightjs';
+import { HighlightLineNumbers } from 'ngx-highlightjs/line-numbers';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+ `,
+ standalone: true,
+ imports: [HighlightAuto, HighlightLineNumbers]
+})
+export class AppComponent {
+}
+```
## Options
-| Name | Type | Description |
-|-------------------|---------------------|------------------------------------------------------------------------------------------------------------|
-| **[highlight]** | string | Accept code string to highlight, default `null` |
-| **[languages]** | string[] | An array of language names and aliases restricting auto detection to only these languages, default: `null` |
-| **[lineNumbers]** | boolean | A flag that indicates adding line numbers to highlighted code element |
-| **(highlighted)** | HighlightAutoResult | Stream that emits the result object when element is highlighted |
+| Name | Type | Description |
+|------------------|---------|--------------------------------------------------------------|
+| **[singleLine]** | boolean | Enable plugin for code block with one line, default `false`. |
+| **[startFrom]** | number | Start numbering from a custom value, default `1`. |
### NOTE
-In Angular 10, when building your project, you might get a
-warning `WARNING in ... CommonJS or AMD dependencies can cause optimization bailouts.`
+During the project build process, you may encounter a warning stating `WARNING in ... CommonJS or AMD dependencies can cause optimization bailouts`.
-To avoid this warning, add the following in your `angular.json`
+To address this warning, include the following configuration in your angular.json file:
```json
{
@@ -222,36 +257,35 @@ To avoid this warning, add the following in your `angular.json`
Read more about [CommonJS dependencies configuration](https://angular.io/guide/build#configuring-commonjs-dependencies)
+
+
+
## Plus package
-This package contains the following features:
+This package provides the following features:
-- Highlight gists using gists API
-- Highlight code directly from URL
+- Utilizes the gists API to highlight code snippets directly from GitHub gists.
+- Supports direct code highlighting from URLs.
### Usage
-The plus package relies on `HttpClient` to make the http requests, ensure that it is imported in your `main.ts` file
+To integrate this addon into your project, ensure the presence of `HttpClient` by importing it into your `main.ts` file.
```ts
import { provideHttpClient } from '@angular/common/http';
-bootstrapApplication(AppComponent, {
+export const appConfig: ApplicationConfig = {
providers: [
- provideHttpClient(),
- {
- provide: HIGHLIGHT_OPTIONS,
- useValue: // ...
- }
+ provideHttpClient()
]
-})
+};
```
### Highlight a gist file
-1. Use `[gist]` directive with the gist id to get the response through the output `(gistLoaded)`.
-2. Once `(gistLoaded)` emits, you will get access to the gist response.
-3. Use `gistContent` pipe to extract the file content from gist response using gist file name.
+1. Use `[gist]` directive, passing the gist ID as its attribute, to retrieve the response through the `(gistLoaded)` output event.
+2. Upon the emission of `(gistLoaded)`, gain access to the gist response.
+3. Use `gistContent` pipe to extract the file's content from the gist response based on the specified file name.
**Example:**
@@ -276,6 +310,8 @@ export class AppComponent {
To loop over `gist?.files`, use `keyvalue` pipe to pass file name into `gistContent` pipe.
+To highlight all files within a gist, iterate through `gist.files` and utilize the `keyvalue` pipe to pass the file name into the `gistContent` pipe.
+
**Example:**
```ts
@@ -285,9 +321,9 @@ import { HighlightPlusModule } from 'ngx-highlightjs';
selector: 'app-root',
template: `
`,
standalone: true,
@@ -299,7 +335,7 @@ export class AppComponent {
### Highlight code from URL directly
-Use the pipe `codeFromUrl` with the `async` pipe together to get the code text from a raw URL.
+Use the pipe `codeFromUrl` with the `async` pipe to get the code text from a raw URL.
**Example:**
@@ -322,39 +358,24 @@ export class AppComponent {
### Providing Gist API secret (Optional)
-To take full advantage of the gist loader feature, the package provides `GIST_OPTIONS` token to set your `clientId`
-and `clientSecret` with the gist http requests, you can provide it in `main.ts` like in this example:
+The package offers the `provideHighlightOptions` function, allowing you to set your `clientId` and `clientSecret` for the gist HTTP requests.
+You can provide these options in your `app.config.ts` file as demonstrated below:
-```typescript
-import { GIST_OPTIONS } from 'ngx-highlightjs/plus'
+```ts
+import { provideHttpClient } from '@angular/common/http';
+import { provideHighlightOptions } from 'ngx-highlightjs/plus'
-bootstrapApplication(AppComponent, {
+export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
- {
- provide: HIGHLIGHT_OPTIONS,
- useValue: // ...
- },
- {
- provide: GIST_OPTIONS,
- useValue: {
- clientId: 'CLIENT_ID',
- clientSecret: 'CLIENT_SECRET'
- }
- },
+ provideGistOptions({
+ clientId: 'CLIENT_ID',
+ clientSecret: 'CLIENT_SECRET'
+ })
]
-})
+};
```
-
-
-## Development
-
-This project uses Angular CLI to build the package.
-
-```bash
-$ ng build ngx-highlightjs
-```
@@ -382,5 +403,3 @@ an [issue](https://github.com/MurhafSousli/ngx-highlightjs/issues).
- [ngx-progressbar](https://github.com/MurhafSousli/ngx-progressbar)
- [ngx-bar-rating](https://github.com/MurhafSousli/ngx-bar-rating)
- [ngx-disqus](https://github.com/MurhafSousli/ngx-disqus)
-- [ngx-wordpress](https://github.com/MurhafSousli/ngx-wordpress)
-- [ngx-teximate](https://github.com/MurhafSousli/ngx-teximate)
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..b391f50 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": "11.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-file-location.ts b/projects/ngx-highlightjs/plus/src/code-file-location.ts
new file mode 100644
index 0000000..afcd8c2
--- /dev/null
+++ b/projects/ngx-highlightjs/plus/src/code-file-location.ts
@@ -0,0 +1,26 @@
+import { inject, InjectionToken } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+
+/**
+ * Injection token used to provide the current location to `codeFromUrl` pipe.
+ * Used to handle server-side rendering and to stub out during unit tests.
+ */
+export const HIGHLIGHT_FILE_LOCATION: InjectionToken = new InjectionToken('HIGHLIGHT_FILE_LOCATION', {
+ providedIn: 'root',
+ factory: CODE_FILE_LOCATION_FACTORY,
+});
+
+export interface CodeFileLocation {
+ getPathname: () => string;
+}
+
+export function CODE_FILE_LOCATION_FACTORY(): CodeFileLocation {
+ const _location: Location = inject(DOCUMENT)?.location;
+
+ return {
+ // Note that this needs to be a function, rather than a property, because Angular
+ // will only resolve it once, but we want the current path on each call.
+ // getPathname: () => (_location ? _location.pathname + _location.search : ''),
+ getPathname: () => (_location ? _location.origin : '')
+ };
+}
diff --git a/projects/ngx-highlightjs/plus/src/code-from-url.ts b/projects/ngx-highlightjs/plus/src/code-from-url.ts
index dcf7cc3..533242f 100644
--- a/projects/ngx-highlightjs/plus/src/code-from-url.ts
+++ b/projects/ngx-highlightjs/plus/src/code-from-url.ts
@@ -1,17 +1,20 @@
-import { Pipe, PipeTransform } from '@angular/core';
+import { inject, Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs';
import { CodeLoader } from './code-loader';
+import { HIGHLIGHT_FILE_LOCATION, CodeFileLocation } from './code-file-location';
+import { isUrl } from './gist.model';
@Pipe({
- name: 'codeFromUrl',
- standalone: true
+ standalone: true,
+ name: 'codeFromUrl'
})
export class CodeFromUrlPipe implements PipeTransform {
- constructor(private _loader: CodeLoader) {
- }
+ private _location: CodeFileLocation = inject(HIGHLIGHT_FILE_LOCATION);
+
+ private _loader: CodeLoader = inject(CodeLoader);
transform(url: string): Observable {
- return this._loader.getCodeFromUrl(url);
+ return this._loader.getCodeFromUrl(isUrl(url) ? url : `${ this._location.getPathname() }/${ url }`);
}
}
diff --git a/projects/ngx-highlightjs/plus/src/code-loader.ts b/projects/ngx-highlightjs/plus/src/code-loader.ts
index c0070bd..d858d1d 100644
--- a/projects/ngx-highlightjs/plus/src/code-loader.ts
+++ b/projects/ngx-highlightjs/plus/src/code-loader.ts
@@ -1,14 +1,16 @@
-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';
+import { Gist, GIST_OPTIONS, GistOptions, isUrl } from './gist.model';
@Injectable({
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
@@ -16,7 +18,7 @@ export class CodeLoader {
*/
getCodeFromGist(id: string): Observable {
let params!: HttpParams;
- if (this.isOAuthProvided()) {
+ if (this._options?.clientId && this._options?.clientSecret) {
params = new HttpParams().set('client_id', this._options.clientId).set('client_secret', this._options.clientSecret);
}
return this.fetchFile(`https://api.github.com/gists/${ id }`, { params, responseType: 'json' });
@@ -30,13 +32,6 @@ export class CodeLoader {
return this.fetchFile(url, { responseType: 'text' });
}
- /**
- * Check if OAuth option is provided
- */
- private isOAuthProvided(): boolean {
- return !!this._options && !!this._options.clientId && !!this._options.clientSecret;
- }
-
private fetchFile(url: string, options: any): Observable {
// Check if URL is valid
if (isUrl(url)) {
@@ -53,8 +48,3 @@ export class CodeLoader {
}
}
-
-function isUrl(url: string) {
- const 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..773a08f 100644
--- a/projects/ngx-highlightjs/plus/src/gist.model.ts
+++ b/projects/ngx-highlightjs/plus/src/gist.model.ts
@@ -1,11 +1,27 @@
-import { InjectionToken } from '@angular/core';
+import { InjectionToken, Provider } from '@angular/core';
+
+
+export function isUrl(url: string): boolean {
+ const regExp: RegExp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!]))?/;
+ return regExp.test(url);
+}
export interface GistOptions {
clientId: string;
clientSecret: string;
}
-export const GIST_OPTIONS = new InjectionToken('GIST_OPTIONS');
+export const GIST_OPTIONS: InjectionToken = new InjectionToken('GIST_OPTIONS');
+
+export function provideGistOptions(options: GistOptions): Provider[] {
+ return [
+ {
+ provide: GIST_OPTIONS,
+ useValue: 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..7ad35c1 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) {
@@ -18,12 +17,12 @@ export class GistDirective {
}
}
- @Output() gistLoad = new EventEmitter();
+ @Output() gistLoad: EventEmitter = new EventEmitter();
}
@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..0c4e095 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,21 +55,23 @@ 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);
- }
+ }
+ // Load highlighting theme
+ 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..80ec8d3 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.model.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.model.ts
@@ -1,130 +1,18 @@
-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 {
- /** classPrefix: a string prefix added before class names in the generated markup, used for backwards compatibility with stylesheets. */
- classPrefix?: string;
- /** 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;
+export interface LineNumbersOptions {
+ startFrom?: number;
+ singleLine?: boolean;
}
export interface HighlightOptions {
- config?: HighlightConfig;
+ config?: Partial;
+ lineNumbersOptions?: LineNumbersOptions;
languages?: Record Promise>;
coreLibraryLoader?: () => Promise;
fullLibraryLoader?: () => Promise;
@@ -132,4 +20,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..8ce1fc7 100644
--- a/projects/ngx-highlightjs/src/lib/highlight.service.ts
+++ b/projects/ngx-highlightjs/src/lib/highlight.service.ts
@@ -1,37 +1,29 @@
-import { Injectable, Inject, Optional } from '@angular/core';
-import { Observable, filter, map, tap } from 'rxjs';
-import {
- HighlightConfig,
- HighlightResult,
- HighlightLibrary,
- HighlightOptions,
- HIGHLIGHT_OPTIONS,
- HighlightAutoResult
-} from './highlight.model';
+import { Injectable, signal, inject, computed, WritableSignal, Signal } from '@angular/core';
+import type { HLJSApi, HighlightResult, AutoHighlightResult, LanguageFn, HLJSOptions } from 'highlight.js';
+import { HighlightOptions, 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);
- if (hljs.listLanguages().length < 1) {
- console.error('[HighlightJS]: No languages were registered!');
- }
+ hljs.configure(this.options.config);
}
});
}
@@ -42,137 +34,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..9d97ef8 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..e7cfcce
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight-auto.spec.ts
@@ -0,0 +1,98 @@
+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 highlightAuto directive add hljs class', () => {
+ expect(directiveInstance).toBeTruthy();
+ 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('');
+ });
+
+ it('[Content-Security-Policy (CSP)] highlight element when trustedTypes is not supported by the browser', async () => {
+ const trustedTypesBackup: any = window['trustedTypes'];
+ delete window['trustedTypes'];
+ component.code = testJsCode;
+ fixture.detectChanges()
+
+ const highlightedCode: string = hljs.highlightAuto(testJsCode, null).value;
+
+ await afterTimeout(200);
+ expect(directiveElement.nativeElement.innerHTML).toBe(highlightedCode);
+
+ window['trustedTypes'] = trustedTypesBackup;
+ });
+});
diff --git a/projects/ngx-highlightjs/src/lib/tests/highlight-options.spec.ts b/projects/ngx-highlightjs/src/lib/tests/highlight-options.spec.ts
new file mode 100644
index 0000000..d823f43
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight-options.spec.ts
@@ -0,0 +1,19 @@
+import { TestBed } from '@angular/core/testing';
+import { HIGHLIGHT_OPTIONS, HighlightOptions, provideHighlightOptions } from 'ngx-highlightjs';
+
+const fullLibraryLoader = () => import('highlight.js');
+
+describe('provideHighlightOptions', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ providers: [
+ provideHighlightOptions({ fullLibraryLoader })
+ ]
+ }).compileComponents();
+ });
+
+ it('should be able to provide options using provideHighlightOptions function', () => {
+ const options: HighlightOptions = TestBed.inject(HIGHLIGHT_OPTIONS);
+ expect(options).toBeTruthy();
+ });
+});
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..cf08d73
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.loader.spec.ts
@@ -0,0 +1,202 @@
+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';
+
+
+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', () => {
+
+ beforeEach(() => {
+ // Clean up hljs
+ document.defaultView['hljs'] = null;
+ });
+
+ 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 load the library when fullLibrary is provided', async () => {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ fullLibraryLoader
+ } as HighlightOptions
+ });
+
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ const lib: HLJSApi = await loader.ready;
+ expect(lib).toBe(hljs);
+ });
+
+ it('should load the library when coreLibrary is provided', async () => {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ coreLibraryLoader,
+ languages: {
+ typescript
+ }
+ } as HighlightOptions
+ });
+
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ const lib: HLJSApi = await loader.ready;
+ expect(lib).toBe(hljs);
+ });
+
+ it('should load the library when lineNumber is provided', async () => {
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ fullLibraryLoader,
+ lineNumbersLoader
+ } as HighlightOptions
+ });
+
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+ const lib: HLJSApi = await loader.ready;
+ expect(lib).toBe(hljs);
+ expect(lib['lineNumbersBlock']).toBeTruthy();
+ });
+
+ 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,
+ 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,
+ languages: {
+ 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
+ } 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
+ }
+ } 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);
+
+ loader.setTheme('https://initial-theme-path.css/');
+
+ const diffPath: string = 'https://different-theme-path.css/';
+
+ loader.setTheme(diffPath);
+ expect(loader['_themeLinkElement'].href).toBe(diffPath);
+ });
+
+ 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);
+ });
+
+
+ it('should load theme on init if themePath option is provided', () => {
+ const themePath: string = 'https://path-to-theme.css/';
+ TestBed.overrideProvider(HIGHLIGHT_OPTIONS, {
+ useValue: {
+ themePath: 'https://path-to-theme.css/'
+ } as HighlightOptions
+ });
+
+ document.defaultView['hljs'] = hljs;
+ const loader: HighlightLoader = TestBed.inject(HighlightLoader);
+
+ expect(loader['_themeLinkElement']).toBeTruthy();
+ expect(loader['_themeLinkElement'].href).toBe(themePath);
+ expect(loader['_themeLinkElement'].type).toBe('text/css');
+ expect(loader['_themeLinkElement'].rel).toBe('stylesheet');
+ expect(loader['_themeLinkElement'].media).toBe('screen,print');
+ });
+});
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..73242a3
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.service.spec.ts
@@ -0,0 +1,180 @@
+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 set the library reference signal when library is loaded', async () => {
+ const service: HighlightJS = TestBed.inject(HighlightJS);
+ await service['loader'].ready;
+ expect(service.hljs()).toEqual(hljs);
+ });
+
+ 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..ab220ea
--- /dev/null
+++ b/projects/ngx-highlightjs/src/lib/tests/highlight.spec.ts
@@ -0,0 +1,93 @@
+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 add hljs class', () => {
+ expect(directiveInstance).toBeTruthy();
+ 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"
- ]
-}
| |