Skip to content

Commit

Permalink
Merge pull request #254 from valerymelou/feature/shiki
Browse files Browse the repository at this point in the history
feat: replace prismjs with shiki
  • Loading branch information
valerymelou authored Jul 26, 2024
2 parents 9a26a48 + ec0f006 commit ecff977
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 69 deletions.
30 changes: 30 additions & 0 deletions apps/website/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,33 @@
.dark .base {
background-image: url('/assets/images/looper-dark.svg');
}

* {
scrollbar-color: #d7d7d7 #fafafa;

.dark & {
scrollbar-color: #333 #080e29;
}
}

/* Chrome, Edge, and Safari */

*::-webkit-scrollbar-track {
background: white;
border-radius: 5px;

.dark & {
background: #011627;
}
}

*::-webkit-scrollbar-thumb {
background-color: white;
border-radius: 14px;
border: 3px solid white;

.dark & {
background-color: #011627;
border: 3px solid #011627;
}
}
12 changes: 1 addition & 11 deletions libs/blog/feature-article/src/lib/blog-article.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,7 @@
>

<ng-container *cfRichTextMark="MARKS.CODE; let node = node">
@if (node.value.split('\n').length > 1) {
<pre
class="bg-dark dark:bg-secondary-dark overflow-auto rounded-md p-4 text-white"
><code [innerHTML]="highlightCode(node.value)"></code></pre>
} @else {
<div
class="bg-dark dark:bg-secondary-dark inline-block rounded-md px-2 leading-5 text-white"
>
<code [innerHTML]="highlightCode(node.value)"></code>
</div>
}
<ui-code [code]="node.value"></ui-code>
</ng-container>
</div>
</div>
Expand Down
55 changes: 9 additions & 46 deletions libs/blog/feature-article/src/lib/blog-article.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Component, Inject, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { CommonModule, DOCUMENT } from '@angular/common';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterModule } from '@angular/router';

import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';

import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-rust';

import {
CfRichTextChildrenDirective,
CfRichTextMarkDirective,
Expand All @@ -19,7 +15,11 @@ import { bootstrapArrowLeft } from '@ng-icons/bootstrap-icons';

import { Article } from '@valerymelou/blog/data-access';
import { MetadataService } from '@valerymelou/shared/seo';
import { ButtonComponent, LinkComponent } from '@valerymelou/shared/ui';
import {
ButtonComponent,
CodeComponent,
LinkComponent,
} from '@valerymelou/shared/ui';

@Component({
selector: 'blog-article',
Expand All @@ -34,11 +34,12 @@ import { ButtonComponent, LinkComponent } from '@valerymelou/shared/ui';
NgIconComponent,
ButtonComponent,
LinkComponent,
CodeComponent,
],
templateUrl: './blog-article.component.html',
viewProviders: [provideIcons({ bootstrapArrowLeft })],
})
export class BlogArticleComponent implements OnInit, OnDestroy {
export class BlogArticleComponent {
article!: Article;
loaded = false;
ready = false;
Expand All @@ -47,13 +48,9 @@ export class BlogArticleComponent implements OnInit, OnDestroy {
readonly MARKS = MARKS;
readonly INLINES = INLINES;

private codeHighlightCssLink!: HTMLLinkElement;

constructor(
private renderer: Renderer2,
private route: ActivatedRoute,
private metadataService: MetadataService,
@Inject(DOCUMENT) private document: Document,
) {
this.route.data.subscribe({
next: (data) => {
Expand All @@ -67,38 +64,4 @@ export class BlogArticleComponent implements OnInit, OnDestroy {
},
});
}

ngOnInit(): void {
this.loadCodeHighlightLib();
}

highlightCode(code: string): string {
let language = code.split('\n')[0].replace('```', '');

if (languages[language]) {
code = code.replace('```' + language + '\n', '');
} else {
language = 'javascript';
}

return highlight(code, languages[language], language);
}

private loadCodeHighlightLib(): void {
this.codeHighlightCssLink = this.renderer.createElement('link');
this.codeHighlightCssLink.rel = 'stylesheet';
this.codeHighlightCssLink.href =
'https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism-okaidia.min.css';
this.codeHighlightCssLink.integrity =
'sha512-5HvW0a7ihK3ro2KhwEksDHXgIezsTeZybZDIn8d8Y015Ny+t7QWSIjnlCTjFzlK7Klb604HLGjsNqU/i5mJLjQ==';
this.codeHighlightCssLink.crossOrigin = 'anonymous';
this.codeHighlightCssLink.referrerPolicy = 'no-referrer';
this.renderer.appendChild(this.document.head, this.codeHighlightCssLink);
}

ngOnDestroy(): void {
if (this.codeHighlightCssLink) {
this.renderer.removeChild(this.document.head, this.codeHighlightCssLink);
}
}
}
2 changes: 2 additions & 0 deletions libs/shared/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './lib/menu/menu.component';
export * from './lib/menu/menu-trigger-for.directive';

export * from './lib/link/link.component';

export * from './lib/code/code.component';
29 changes: 29 additions & 0 deletions libs/shared/ui/src/lib/code/code.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@if (code) {
@if (code.split(' ').length === 1) {
<div [innerHTML]="highlightedCode" class="code inline-flex"></div>
} @else {
<div class="group relative">
<span
class="absolute right-2 top-2 text-xs text-slate-400 opacity-100 transition-all duration-300 ease-in-out group-hover:opacity-0"
>{{ language }}</span
>
<button
class="absolute right-2 top-2 flex h-10 min-w-10 items-center justify-center rounded-lg border border-slate-400 bg-slate-100 text-slate-400 opacity-0 transition-all duration-300 ease-in-out group-hover:opacity-100 dark:border-slate-400 dark:bg-slate-900"
aria-label="Copy"
[ngClass]="{ 'px-4': copied }"
(click)="copy()"
>
@if (copied) {
<span class="text-sm">Copied</span>
<ng-icon name="bootstrapCheck" size="20"></ng-icon>
} @else {
<ng-icon name="bootstrapCopy"></ng-icon>
}
</button>
<div
[innerHTML]="highlightedCode"
class="code dark:border-secondary-dark overflow-x-auto rounded-md border border-slate-100"
></div>
</div>
}
}
25 changes: 25 additions & 0 deletions libs/shared/ui/src/lib/code/code.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:host {
display: inline-block;
width: 100%;
}

pre {
padding: 24px;
border-radius: 4px;
}

.inline-flex {
pre {
padding: 0 6px;
}
}

html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
44 changes: 44 additions & 0 deletions libs/shared/ui/src/lib/code/code.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CodeComponent } from './code.component';
import { By } from '@angular/platform-browser';
import { WINDOW_TOKEN } from '@valerymelou/common/browser';

describe('CodeComponent', () => {
let component: CodeComponent;
let fixture: ComponentFixture<CodeComponent>;
const code = '```html\n<h1>Hello World</h1><div>This is a test</div>\n';

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CodeComponent],
providers: [
{
provide: WINDOW_TOKEN,
useValue: {
navigator: { clipboard: { writeText: () => Promise.resolve() } },
},
},
],
}).compileComponents();

fixture = TestBed.createComponent(CodeComponent);
component = fixture.componentInstance;
component.code = code;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should copy the code to the clipboard', () => {
const window = TestBed.inject(WINDOW_TOKEN);
const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
const button = fixture.debugElement.query(By.css('button'));

button.triggerEventHandler('click');
expect(writeTextSpy).toHaveBeenCalledWith(
code.replace('```html\n', '').replace('\n```', ''),
);
});
});
79 changes: 79 additions & 0 deletions libs/shared/ui/src/lib/code/code.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Component, Inject, Input, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { bootstrapCopy, bootstrapCheck } from '@ng-icons/bootstrap-icons';
import { bundledLanguages } from 'shiki/langs';
import { codeToHtml } from 'shiki';
import { WINDOW_TOKEN } from '@valerymelou/common/browser';

@Component({
selector: 'ui-code',
standalone: true,
imports: [CommonModule, NgIconComponent],
templateUrl: './code.component.html',
styleUrls: ['./code.component.scss'],
encapsulation: ViewEncapsulation.None,
viewProviders: [provideIcons({ bootstrapCopy, bootstrapCheck })],
})
export class CodeComponent {
@Input()
set code(value: string) {
this._code = value;
this.highlight();
}

get code(): string {
return this._code;
}

@Input() language!: string;
highlightedCode!: SafeHtml;

private _code!: string;
copied = false;

constructor(
private sanitizer: DomSanitizer,
@Inject(WINDOW_TOKEN) private window: Window,
) {}

copy(): void {
this.window.navigator.clipboard.writeText(this.code).then(() => {
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 3000);
});
}

highlight(): void {
this.language =
this.language ?? this.code.split('\n')[0].replace('```', '');

if (this.language in bundledLanguages) {
this._code = this.code.replace('```' + this.language + '\n', '');
} else {
this.language = 'javascript';
}

codeToHtml(this.code, {
lang: this.language,
themes: {
light: 'catppuccin-latte',
dark: 'material-theme-ocean',
},
transformers: [
{
pre(node) {
this.addClassToHast(node, 'overflow-x-auto');
},
},
],
}).then((highlightedCode) => {
this.highlightedCode =
this.sanitizer.bypassSecurityTrustHtml(highlightedCode);
});
}
}
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@ng-icons/material-icons": "^29.0.0",
"contentful": "^10.12.13",
"express": "~4.19.2",
"prismjs": "^1.29.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
Expand Down Expand Up @@ -54,7 +53,6 @@
"@types/express": "4.17.21",
"@types/jest": "^29.4.0",
"@types/node": "18.16.9",
"@types/prismjs": "^1.26.4",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@typescript-eslint/utils": "^7.16.0",
Expand All @@ -73,6 +71,7 @@
"postcss-url": "~10.1.3",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.6.5",
"shiki": "^1.11.1",
"tailwindcss": "^3.4.4",
"ts-jest": "^29.1.0",
"ts-node": "10.9.2",
Expand Down
Loading

0 comments on commit ecff977

Please sign in to comment.