Files
ui-essentials/projects/ui-code-display/src/lib/components/code-block/code-block.component.ts
skyai_dev 246c62fd49 Add landing pages library with comprehensive components and demos
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 13:52:41 +10:00

175 lines
5.3 KiB
TypeScript

import { Component, Input, computed, signal, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SyntaxHighlighterService } from '../../services/syntax-highlighter.service';
import { CodeThemeService, CodeTheme } from '../../services/theme.service';
import { CopyButtonDirective } from '../../directives/copy-button.directive';
export type CodeBlockVariant = 'default' | 'minimal' | 'bordered';
@Component({
selector: 'ui-code-block',
standalone: true,
imports: [CommonModule, CopyButtonDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<div [class]="containerClasses()">
@if (copyable) {
<button
type="button"
class="code-block__copy-btn"
[uiCopyButton]="code"
[attr.aria-label]="'Copy code to clipboard'"
title="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
}
<pre [class]="preClasses()">
@if (showLineNumbers && lineCount() > 1) {
<div class="code-block__line-numbers" [attr.aria-hidden]="true">
@for (lineNum of lineNumbers(); track lineNum) {
<div
class="code-block__line-number"
[class.code-block__line-number--highlighted]="isLineHighlighted(lineNum)">
{{ lineNum }}
</div>
}
</div>
}
<code
[class]="codeClasses()"
[innerHTML]="highlightedCode()">
</code>
</pre>
</div>
`,
styleUrl: './code-block.component.scss'
})
export class CodeBlockComponent implements OnInit, OnChanges {
@Input() code = '';
@Input() language = '';
@Input() theme: CodeTheme | null = null;
@Input() variant: CodeBlockVariant = 'default';
@Input() showLineNumbers = true;
@Input() copyable = true;
@Input() highlightLines: (number | string)[] = [];
@Input() startLineNumber = 1;
@Input() wrap = false;
constructor(
private syntaxHighlighter: SyntaxHighlighterService,
private themeService: CodeThemeService
) {}
readonly finalLanguage = computed(() => {
if (this.language) {
return this.language;
}
return this.syntaxHighlighter.detectLanguage(this.code);
});
readonly highlightedCode = signal('');
private async updateHighlightedCode() {
if (!this.code) {
this.highlightedCode.set('');
return;
}
const lang = this.finalLanguage();
try {
const highlighted = await this.syntaxHighlighter.highlight(this.code, {
language: lang,
lineNumbers: this.showLineNumbers,
highlightLines: this.highlightLines
});
this.highlightedCode.set(highlighted);
} catch (error) {
console.warn('Failed to highlight code:', error);
this.highlightedCode.set(this.escapeHtml(this.code));
}
}
readonly lineCount = computed(() => {
if (!this.code) return 0;
const lines = this.code.split('\n');
// Remove the last empty line if the code ends with a newline
if (lines.length > 1 && lines[lines.length - 1] === '') {
return lines.length - 1;
}
return lines.length;
});
readonly lineNumbers = computed(() => {
const count = this.lineCount();
return Array.from({ length: count }, (_, i) => i + this.startLineNumber);
});
readonly currentTheme = computed(() => {
return this.theme || this.themeService.theme();
});
readonly containerClasses = computed(() => {
const classes = ['code-block'];
classes.push(`code-block--${this.variant}`);
classes.push(`code-theme-${this.currentTheme()}`);
if (this.wrap) classes.push('code-block--wrap');
if (this.copyable) classes.push('code-block--copyable');
return classes.join(' ');
});
readonly preClasses = computed(() => {
const classes = ['code-block__pre'];
if (this.showLineNumbers && this.lineCount() > 1) {
classes.push('code-block__pre--with-line-numbers');
}
return classes.join(' ');
});
readonly codeClasses = computed(() => {
const classes = ['code-block__code'];
classes.push(`language-${this.finalLanguage()}`);
return classes.join(' ');
});
isLineHighlighted(lineNumber: number): boolean {
return this.highlightLines.some(highlight => {
if (typeof highlight === 'number') {
return highlight === lineNumber;
}
// Handle range format like "5-10"
const range = highlight.toString().split('-');
if (range.length === 2) {
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
return lineNumber >= start && lineNumber <= end;
}
return false;
});
}
ngOnInit(): void {
this.updateHighlightedCode();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['code'] || changes['language']) {
this.updateHighlightedCode();
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}