Skip to content

Commit a1aa700

Browse files
committed
feat: ssr
1 parent 945add3 commit a1aa700

File tree

9 files changed

+105
-148
lines changed

9 files changed

+105
-148
lines changed

projects/ngx-highlightjs-demo/src/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { provideAnimationsAsync } from '@angular/platform-browser/animations/asy
44
import { provideHighlightOptions } from 'ngx-highlightjs';
55
import { provideGistOptions } from 'ngx-highlightjs/plus';
66
import { environment } from '../environments/environment';
7+
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
78

89
export const appConfig: ApplicationConfig = {
910
providers: [
11+
provideClientHydration(withEventReplay()),
1012
provideHttpClient(withFetch()),
1113
provideHighlightOptions({
1214
// fullLibraryLoader: () => import('highlight.js'),

projects/ngx-highlightjs/line-numbers/src/line-numbers-lib.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
export function activateLineNumbers() {
2-
const w: any = window;
3-
const d: Document = document;
1+
import { type HLJSApi } from "highlight.js";
42

3+
export function activateLineNumbers(hljs: HLJSApi, document: Document) {
54
const TABLE_NAME: string = 'hljs-ln',
65
LINE_NAME: string = 'hljs-ln-line',
76
CODE_BLOCK_NAME: string = 'hljs-ln-code',
@@ -10,15 +9,11 @@ export function activateLineNumbers() {
109
DATA_ATTR_NAME: string = 'data-line-number',
1110
BREAK_LINE_REGEXP: RegExp = /\r\n|\r|\n/g;
1211

13-
if (w.hljs) {
14-
w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad;
15-
w.hljs.lineNumbersBlock = lineNumbersBlock;
16-
w.hljs.lineNumbersValue = lineNumbersValue;
12+
(hljs as any).initLineNumbersOnLoad = initLineNumbersOnLoad;
13+
(hljs as any).lineNumbersBlock = lineNumbersBlock;
14+
(hljs as any).lineNumbersValue = lineNumbersValue;
1715

18-
addStyles();
19-
} else {
20-
w.console.error('highlight.js not detected!');
21-
}
16+
addStyles();
2217

2318
function isHljsLnCodeDescendant(domElt): boolean {
2419
let curElt = domElt;
@@ -128,7 +123,7 @@ export function activateLineNumbers() {
128123
});
129124

130125
function addStyles() {
131-
const css: HTMLStyleElement = d.createElement('style');
126+
const css: HTMLStyleElement = document.createElement('style');
132127
css.type = 'text/css';
133128
css.innerHTML = format(
134129
'.{0}{border-collapse:collapse}' +
@@ -139,22 +134,22 @@ export function activateLineNumbers() {
139134
NUMBER_LINE_NAME,
140135
DATA_ATTR_NAME
141136
]);
142-
d.getElementsByTagName('head')[0].appendChild(css);
137+
document.getElementsByTagName('head')[0].appendChild(css);
143138
}
144139

145140
function initLineNumbersOnLoad(options) {
146-
if (d.readyState === 'interactive' || d.readyState === 'complete') {
141+
if (document.readyState === 'interactive' || document.readyState === 'complete') {
147142
documentReady(options);
148143
} else {
149-
w.addEventListener('DOMContentLoaded', function () {
144+
document.addEventListener('DOMContentLoaded', function () {
150145
documentReady(options);
151146
});
152147
}
153148
}
154149

155150
function documentReady(options): void {
156151
try {
157-
const blocks: NodeListOf<Element> = d.querySelectorAll('code.hljs,code.nohighlight');
152+
const blocks: NodeListOf<Element> = document.querySelectorAll('code.hljs,code.nohighlight');
158153

159154
for (const i in blocks) {
160155
if (blocks.hasOwnProperty(i)) {
@@ -164,7 +159,7 @@ export function activateLineNumbers() {
164159
}
165160
}
166161
} catch (e) {
167-
w.console.error('LineNumbers error: ', e);
162+
console.error('LineNumbers error: ', e);
168163
}
169164
}
170165

@@ -177,9 +172,7 @@ export function activateLineNumbers() {
177172
return;
178173
}
179174

180-
async(function () {
181-
element.innerHTML = lineNumbersInternal(element, options);
182-
});
175+
element.innerHTML = lineNumbersInternal(element, options);
183176
}
184177

185178
function lineNumbersValue(value, options) {
@@ -336,10 +329,6 @@ export function activateLineNumbers() {
336329
/// HELPERS
337330
///
338331

339-
function async(func) {
340-
w.setTimeout(func, 0);
341-
}
342-
343332
/**
344333
* {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript}
345334
* @param {string} format

projects/ngx-highlightjs/line-numbers/src/line-numbers.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {
33
inject,
44
numberAttribute,
55
booleanAttribute,
6-
afterRenderEffect,
76
input,
87
ElementRef,
9-
InputSignalWithTransform
8+
InputSignalWithTransform,
9+
effect
1010
} from '@angular/core';
1111
import { HIGHLIGHT_OPTIONS, HighlightJS, HighlightBase, LineNumbersOptions } from 'ngx-highlightjs';
1212

@@ -20,9 +20,6 @@ export class HighlightLineNumbers {
2020
private readonly _highlight: HighlightBase = inject(HighlightBase);
2121
private readonly _nativeElement: HTMLElement = inject(ElementRef<HTMLElement>).nativeElement;
2222

23-
// Temp observer to observe when line numbers has been added to code element
24-
private _lineNumbersObs: MutationObserver;
25-
2623
readonly startFrom: InputSignalWithTransform<number, unknown> = input<number, unknown>(this.options?.startFrom, {
2724
transform: numberAttribute
2825
});
@@ -32,40 +29,24 @@ export class HighlightLineNumbers {
3229
});
3330

3431
constructor() {
35-
afterRenderEffect({
36-
write: () => {
37-
if (this._highlight.highlightResult()) {
38-
this.addLineNumbers();
39-
}
32+
effect(() => {
33+
if (this._highlight.highlightResult()) {
34+
this.addLineNumbers();
4035
}
4136
});
4237
}
4338

44-
private addLineNumbers(): void {
45-
// Clean up line numbers observer
46-
this.destroyLineNumbersObserver();
47-
requestAnimationFrame(async () => {
48-
// Add line numbers
49-
await this._hljs.lineNumbersBlock(this._nativeElement, {
50-
startFrom: this.startFrom(),
51-
singleLine: this.singleLine()
52-
});
53-
// If lines count is 1, the line numbers library will not add numbers
54-
// Observe changes to add 'hljs-line-numbers' class only when line numbers is added to the code element
55-
this._lineNumbersObs = new MutationObserver(() => {
56-
if (this._nativeElement.firstElementChild?.tagName.toUpperCase() === 'TABLE') {
57-
this._nativeElement.classList.add('hljs-line-numbers');
58-
}
59-
this.destroyLineNumbersObserver();
60-
});
61-
this._lineNumbersObs.observe(this._nativeElement, { childList: true });
39+
private async addLineNumbers(): Promise<void> {
40+
// Add line numbers
41+
await this._hljs.lineNumbersBlock(this._nativeElement, {
42+
startFrom: this.startFrom(),
43+
singleLine: this.singleLine()
6244
});
63-
}
64-
65-
private destroyLineNumbersObserver(): void {
66-
if (this._lineNumbersObs) {
67-
this._lineNumbersObs.disconnect();
68-
this._lineNumbersObs = null;
45+
// If lines count is 1, the line numbers library will not add numbers
46+
if (this._nativeElement.firstElementChild?.tagName.toUpperCase() === 'TABLE') {
47+
this._nativeElement.classList.add('hljs-line-numbers');
48+
} else {
49+
this._nativeElement.classList.remove('hljs-line-numbers');
6950
}
7051
}
7152
}

projects/ngx-highlightjs/plus/src/code-from-url.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { inject, Pipe, PipeTransform } from '@angular/core';
2-
import { Observable } from 'rxjs';
1+
import { inject, PendingTasks, Pipe, PipeTransform } from '@angular/core';
2+
import { Observable, tap } from 'rxjs';
33
import { CodeLoader } from './code-loader';
44
import { HIGHLIGHT_FILE_LOCATION, CodeFileLocation } from './code-file-location';
55
import { isUrl } from './gist.model';
@@ -13,7 +13,11 @@ export class CodeFromUrlPipe implements PipeTransform {
1313

1414
private _loader: CodeLoader = inject(CodeLoader);
1515

16+
private _pendingTasks: PendingTasks = inject(PendingTasks);
17+
1618
transform(url: string): Observable<string> {
17-
return this._loader.getCodeFromUrl(isUrl(url) ? url : `${ this._location.getPathname() }/${ url }`);
19+
const done = this._pendingTasks.add()
20+
return this._loader.getCodeFromUrl(isUrl(url) ? url : `${this._location.getPathname()}/${url}`)
21+
.pipe(tap(done));
1822
}
1923
}

projects/ngx-highlightjs/plus/src/gist.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Directive, Pipe, Input, Output, PipeTransform, EventEmitter, inject } from '@angular/core';
1+
import { Directive, Pipe, Input, Output, PipeTransform, EventEmitter, inject, PendingTasks } from '@angular/core';
22
import { CodeLoader } from './code-loader';
33
import { Gist } from './gist.model';
44

@@ -9,10 +9,16 @@ export class GistDirective {
99

1010
private _loader: CodeLoader = inject(CodeLoader);
1111

12+
private _pendingTasks: PendingTasks = inject(PendingTasks);
13+
1214
@Input()
1315
set gist(value: string) {
1416
if (value) {
15-
this._loader.getCodeFromGist(value).subscribe((gist: Gist) => this.gistLoad.emit(gist));
17+
const done = this._pendingTasks.add()
18+
this._loader.getCodeFromGist(value).subscribe((gist: Gist) => {
19+
done()
20+
this.gistLoad.emit(gist)
21+
});
1622
}
1723
}
1824

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {
22
Directive,
33
inject,
4-
afterRenderEffect,
54
ElementRef,
65
InputSignal,
76
WritableSignal,
87
SecurityContext,
9-
OutputEmitterRef
8+
OutputEmitterRef,
9+
effect
1010
} from '@angular/core';
1111
import { DomSanitizer } from '@angular/platform-browser';
1212
import type { AutoHighlightResult, HighlightResult } from 'highlight.js';
@@ -32,40 +32,44 @@ export abstract class HighlightBase {
3232

3333

3434
constructor() {
35-
afterRenderEffect({
36-
write: () => {
37-
const code: string = this.code();
38-
// Set code text before highlighting
35+
let codeInitial = true
36+
effect(() => {
37+
const code: string = this.code();
38+
// Set code text before highlighting
39+
if (codeInitial) {
40+
// Effects run once to create dependency tree, avoid setting initial undefined content
41+
codeInitial = false
42+
} else {
3943
this.setTextContent(code || '');
40-
if (code) {
41-
this.highlightElement(code);
42-
}
44+
}
45+
if (code) {
46+
this.highlightElement(code);
4347
}
4448
});
4549

46-
afterRenderEffect({
47-
write: () => {
48-
const res: AutoHighlightResult = this.highlightResult();
50+
let resultInitial = true
51+
effect(() => {
52+
const res: AutoHighlightResult = this.highlightResult();
53+
if (resultInitial) {
54+
// Effects run once to create dependency tree, avoid setting initial undefined content
55+
resultInitial = false
56+
} else {
4957
this.setInnerHTML(res?.value);
5058
// Forward highlight response to the highlighted output
5159
this.highlighted.emit(res);
5260
}
5361
});
5462
}
5563

56-
protected abstract highlightElement(code: string): Promise<void> ;
64+
protected abstract highlightElement(code: string): Promise<void>;
5765

5866
private setTextContent(content: string): void {
59-
requestAnimationFrame(() =>
60-
this._nativeElement.textContent = content
61-
);
67+
this._nativeElement.textContent = content
6268
}
6369

6470
private setInnerHTML(content: string | null): void {
65-
requestAnimationFrame(() =>
66-
this._nativeElement.innerHTML = trustedHTMLFromStringBypass(
67-
this._sanitizer.sanitize(SecurityContext.HTML, content) || ''
68-
)
69-
);
71+
this._nativeElement.innerHTML = trustedHTMLFromStringBypass(
72+
this._sanitizer.sanitize(SecurityContext.HTML, content) || ''
73+
)
7074
}
7175
}

0 commit comments

Comments
 (0)