Skip to content

Commit 2fc6690

Browse files
committed
Add line numbers
1 parent 6021761 commit 2fc6690

File tree

9 files changed

+186
-26
lines changed

9 files changed

+186
-26
lines changed

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@angular/platform-browser": ">=12 <15",
3333
"@ngneat/until-destroy": "^9.2.1",
3434
"highlight.js": "^11.6.0",
35+
"highlightjs-line-numbers.js": "^2.8.0",
3536
"io-ts": "^2.2.17",
3637
"io-ts-types": "^0.5.16"
3738
},

src/app/app.component.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ import { Component } from '@angular/core';
1414
<hr />
1515
1616
<h4>FETCHED GIST (AUTO CACHED FOR 24 HOURS)</h4>
17+
<p>
18+
ngx-gist will fetch the gist once and store it locally for 24 hours. In
19+
that timeframe if the user returns or visits another page where this
20+
gist was previously loaded, it will reload the content without having to
21+
reach out to GitHub again.
22+
</p>
1723
<ngx-gist gistId="d55ea012b585a16a9970878d90106d74"></ngx-gist>
1824
1925
<h4>FETCHED GIST (FORCED NO CACHE)</h4>
26+
<p>
27+
Force no cache. This will force ngx-gist to retrieve the content live
28+
from GitHub every time this content loads. This is disabled by default,
29+
but could be useful if your gists change frequently.
30+
</p>
2031
<ngx-gist
2132
gistId="d55ea012b585a16a9970878d90106d74"
2233
[useCache]="false"
@@ -45,6 +56,15 @@ import { Component } from '@angular/core';
4556
is displayed here.
4657
</p>
4758
<ngx-gist [gist]="localGistObject"></ngx-gist>
59+
60+
<h4>HIDING LINE NUMBERS</h4>
61+
<p>
62+
Line numbers are enabled by default, but you can turn them off like so.
63+
</p>
64+
<ngx-gist
65+
gistId="d55ea012b585a16a9970878d90106d74"
66+
[showLineNumbers]="false"
67+
></ngx-gist>
4868
</ngx-body>
4969
<ngx-footer #footer></ngx-footer>
5070
`,

src/app/public/ngx-gist-file-filter.pipe.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export class GistFileFilterPipe implements PipeTransform {
1313
return [];
1414
}
1515

16-
console.log(displayOnlyFileNames);
17-
1816
if (!displayOnlyFileNames || displayOnlyFileNames === '') {
1917
return files;
2018
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { DOCUMENT } from '@angular/common';
2+
import { Inject, Injectable } from '@angular/core';
3+
import hljs, { HLJSApi } from 'highlight.js';
4+
import { filter, map, Observable, firstValueFrom, from } from 'rxjs';
5+
6+
@Injectable({ providedIn: 'root' }) // Must be a singleton
7+
export class NgxGistLineNumbersService {
8+
public constructor(@Inject(DOCUMENT) private readonly document: Document) {}
9+
private isLoaded = false;
10+
11+
public async load(): Promise<void> {
12+
if (this.isLoaded) {
13+
return;
14+
}
15+
16+
try {
17+
if (this.document.defaultView) {
18+
// Ensure hljs is available before we load the dependant library
19+
// `highlightjs-line-numbers.js` dynamically as a js import.
20+
this.document.defaultView.hljs = hljs;
21+
} else {
22+
throw new Error(
23+
`Unable to access default view to apply "highlight.js" package.`,
24+
);
25+
}
26+
27+
firstValueFrom(this.loadHljsLineNumbersLibrary()).then(() => {
28+
// The library `highlightjs-line-numbers.js` adds new functions to the
29+
// `highlight.js` scope on load, so we should now be able to call it
30+
// without failure.
31+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
32+
this.document.defaultView?.hljs?.initLineNumbersOnLoad!();
33+
});
34+
} catch (e: unknown) {
35+
console.log(e);
36+
} finally {
37+
this.isLoaded = true;
38+
}
39+
}
40+
41+
/* eslint-disable @typescript-eslint/no-explicit-any */
42+
private loadHljsLineNumbersLibrary(): Observable<any> {
43+
return from(import('highlightjs-line-numbers.js' as any)).pipe(
44+
filter((module: any) => !!module && !!module.default),
45+
map((module: any) => module.default),
46+
);
47+
}
48+
/* eslint-enable @typescript-eslint/no-explicit-any */
49+
}
50+
51+
declare global {
52+
interface Window {
53+
hljs?: HLJSApi & {
54+
/* eslint-disable @typescript-eslint/no-explicit-any */
55+
initLineNumbersOnLoad?: (options?: any) => void;
56+
lineNumbersBlock?: (value: Element, options?: any) => void;
57+
lineNumbersValue?: (value: string, options?: any) => string;
58+
/* eslint-enable @typescript-eslint/no-explicit-any */
59+
};
60+
}
61+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Inject, Injectable } from '@angular/core';
2+
import { DOCUMENT } from '@angular/common';
3+
4+
@Injectable({ providedIn: 'root' }) // Must be a singleton
5+
export class NgxGistThemeService {
6+
public constructor(@Inject(DOCUMENT) private readonly document: Document) {}
7+
8+
private importElMaterialTheme: HTMLLinkElement | null = null;
9+
10+
public setTheme(materialPrebuiltTheme: MaterialPrebuiltTheme): void {
11+
const themeId = 'material-theme-import';
12+
const currentEl = this.document.getElementById(themeId);
13+
14+
if (currentEl) {
15+
this.document.removeChild(currentEl);
16+
}
17+
18+
if (this.importElMaterialTheme) {
19+
this.document.removeChild(this.importElMaterialTheme);
20+
}
21+
22+
this.importElMaterialTheme = this.document.createElement('link');
23+
this.importElMaterialTheme.href = `https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/${materialPrebuiltTheme}.css`;
24+
this.importElMaterialTheme.media = 'screen,print';
25+
this.importElMaterialTheme.rel = 'stylesheet';
26+
this.importElMaterialTheme.type = 'text/css';
27+
this.importElMaterialTheme.id = themeId;
28+
29+
this.document.head.appendChild(this.importElMaterialTheme);
30+
}
31+
}
32+
33+
export type MaterialPrebuiltTheme =
34+
| 'deeppurple-amber'
35+
| 'indigo-pink'
36+
| 'pink-bluegrey'
37+
| 'purple-green';

src/app/public/ngx-gist.component.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { Language } from 'highlight.js';
66
import { BehaviorSubject, filter, firstValueFrom, ReplaySubject } from 'rxjs';
77
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
88
import { DOCUMENT } from '@angular/common';
9+
import { NgxGistLineNumbersService } from './ngx-gist-line-numbers.service';
10+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
11+
import {
12+
MaterialPrebuiltTheme,
13+
NgxGistThemeService,
14+
} from './ngx-gist-theme.service';
915

1016
@UntilDestroy()
1117
@Component({
@@ -21,7 +27,13 @@ import { DOCUMENT } from '@angular/common';
2127
[label]="file.filename"
2228
>
2329
<pre>
24-
<code [innerHTML]="file.highlightedContent"></code>
30+
<code
31+
*ngIf="applyLineNumbers(file.highlightedContent) as content"
32+
[innerHTML]="content"
33+
></code>
34+
<ng-template #error>
35+
<code>Error loading code...</code>
36+
</ng-template>
2537
</pre>
2638
</mat-tab>
2739
</mat-tab-group>
@@ -43,14 +55,12 @@ import { DOCUMENT } from '@angular/common';
4355
})
4456
export class NgxGistComponent implements OnInit {
4557
public constructor(
46-
@Inject(DOCUMENT)
47-
private readonly document: Document,
58+
@Inject(DOCUMENT) private readonly document: Document,
59+
private readonly domSanitizer: DomSanitizer,
4860
private readonly ngxGistService: NgxGistService,
61+
private readonly ngxGistLineNumbersService: NgxGistLineNumbersService,
62+
private readonly ngxGistThemeService: NgxGistThemeService,
4963
) {}
50-
51-
public codeSnippet: string | null = null;
52-
private htmlLinkElement: HTMLLinkElement | null = null;
53-
5464
/**
5565
* Display in the DOM only the selected filename(s) from the gists files array.
5666
*
@@ -122,12 +132,13 @@ export class NgxGistComponent implements OnInit {
122132
* Tip: See theming Angular Material: https://material.angular.io/guide/theming
123133
* if you need help applying a global material theme.
124134
*/
125-
@Input() public materialTheme:
126-
| 'deeppurple-amber'
127-
| 'indigo-pink'
128-
| 'pink-bluegrey'
129-
| 'purple-green'
130-
| undefined = undefined;
135+
@Input() public materialTheme: MaterialPrebuiltTheme | undefined = undefined;
136+
/**
137+
* Display or hide the line numbers in your gist code snippets.
138+
*
139+
* Default: `true`
140+
*/
141+
@Input() public showLineNumbers = true;
131142
/**
132143
* Cache the GitHub gist request in local memory for 24 hours. GitHub has a
133144
* request limit, so this helps in reducing bandwidth. Loads previously
@@ -140,6 +151,10 @@ export class NgxGistComponent implements OnInit {
140151
public async ngOnInit(): Promise<void> {
141152
this.setTheme();
142153

154+
if (this.showLineNumbers) {
155+
await this.ngxGistLineNumbersService.load();
156+
}
157+
143158
this.gistIdChanges
144159
.pipe(filter(isNonEmptyValue), untilDestroyed(this))
145160
.subscribe(async (gistId) => {
@@ -174,12 +189,19 @@ export class NgxGistComponent implements OnInit {
174189
if (!this.materialTheme) {
175190
return;
176191
}
192+
this.ngxGistThemeService.setTheme(this.materialTheme);
193+
}
177194

178-
this.htmlLinkElement = this.document.createElement('link');
179-
this.htmlLinkElement.href = `https://unpkg.com/@angular/material@14.1.0/prebuilt-themes/${this.materialTheme}.css`;
180-
this.htmlLinkElement.media = 'screen,print';
181-
this.htmlLinkElement.rel = 'stylesheet';
182-
this.htmlLinkElement.type = 'text/css';
183-
this.document.head.appendChild(this.htmlLinkElement);
195+
public applyLineNumbers(highlightedConent: string): SafeHtml | null {
196+
if (
197+
this.showLineNumbers &&
198+
this.document.defaultView?.hljs &&
199+
typeof this.document.defaultView.hljs.lineNumbersValue === 'function'
200+
) {
201+
return this.domSanitizer.bypassSecurityTrustHtml(
202+
this.document.defaultView.hljs.lineNumbersValue(highlightedConent),
203+
);
204+
}
205+
return highlightedConent;
184206
}
185207
}

src/app/public/ngx-gist.model.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
isNonEmptyString,
66
parsedJsonFromStringCodec,
77
} from './ngx-gist.utilities';
8-
import { default as hljs } from 'highlight.js';
8+
import hljs from 'highlight.js';
99

1010
export class NgxGist implements Gist {
1111
public constructor(args: Gist & Pick<NgxGist, 'languageOverride'>) {
@@ -167,11 +167,17 @@ function getHighlightedContent(
167167
baseContent: string,
168168
languageOverride?: string,
169169
): string {
170+
let highlighted = baseContent;
171+
170172
if (languageOverride) {
171-
return hljs.highlight(baseContent, { language: languageOverride }).value;
173+
highlighted = hljs.highlight(baseContent, {
174+
language: languageOverride,
175+
}).value;
176+
} else {
177+
highlighted = hljs.highlightAuto(baseContent).value;
172178
}
173179

174-
return hljs.highlightAuto(baseContent).value;
180+
return highlighted;
175181
}
176182

177183
const gitHubUserCodec = io.readonly(

src/app/public/ngx-gist.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { MatCardModule } from '@angular/material/card';
66
import { MatTabsModule } from '@angular/material/tabs';
77
import { GistFileFilterPipe } from './ngx-gist-file-filter.pipe';
88
import { MatIconModule } from '@angular/material/icon';
9+
import { NgxGistLineNumbersService } from './ngx-gist-line-numbers.service';
10+
import { NgxGistThemeService } from './ngx-gist-theme.service';
911

1012
@NgModule({
1113
declarations: [NgxGistComponent, GistFileFilterPipe],
@@ -20,6 +22,6 @@ import { MatIconModule } from '@angular/material/icon';
2022
MatTabsModule,
2123
],
2224
exports: [NgxGistComponent],
23-
providers: [NgxGistService],
25+
providers: [NgxGistLineNumbersService, NgxGistService, NgxGistThemeService],
2426
})
2527
export class NgxGistModule {}

0 commit comments

Comments
 (0)