chore: update Angular TypeScript build info cache

This commit is contained in:
2025-09-28 21:19:15 -04:00
parent 963d0cc323
commit a8b60f87f6
82 changed files with 2378 additions and 410 deletions
+32
View File
@@ -0,0 +1,32 @@
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { MarkdownService } from './markdown.service.js';
import type { Note } from '../types';
const service = new MarkdownService();
const notes: Note[] = [];
const render = (markdown: string) => service.render(markdown, notes);
test('renders tables with inline markdown content', () => {
const html = render(`| Syntax | Description |\n| --- | --- |\n| **bold** | \`code\` |\n`);
assert.ok(html.includes('<table'));
assert.ok(html.includes('<strong>bold</strong>'));
assert.ok(html.includes('<code class="inline-code">code</code>'));
});
test('applies alignment markers for columns', () => {
const html = render(`| Left | Center | Right |\n| :-- | :-: | --: |\n| one | two | three |\n`);
assert.ok(html.includes('style="text-align:left;"'));
assert.ok(html.includes('style="text-align:center;"'));
assert.ok(html.includes('style="text-align:right;"'));
});
test('falls back gracefully when separator row is malformed', () => {
const html = render(`| A | B |\n| -- | |\n| 1 | 2 |\n`);
assert.ok(!html.includes('<div class="markdown-table"><table>'));
assert.ok(html.includes('| A | B |'));
});
+222 -21
View File
@@ -4,6 +4,8 @@ import MarkdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItTaskLists from 'markdown-it-task-lists';
import markdownItAttrs from 'markdown-it-attrs';
import markdownItFootnote from 'markdown-it-footnote';
import markdownItMultimdTable from 'markdown-it-multimd-table';
import { Note } from '../types';
interface MarkdownRenderEnv {
@@ -19,6 +21,27 @@ interface MarkdownItToken {
attrJoin(name: string, value: string): void;
}
interface WikiLinkPlaceholder {
placeholder: string;
alias: string;
target: string;
headingSlug?: string;
headingText?: string;
block?: string;
}
interface MathPlaceholder {
placeholder: string;
expression: string;
display: 'block' | 'inline';
}
interface PreprocessResult {
markdown: string;
wikiLinks: WikiLinkPlaceholder[];
math: MathPlaceholder[];
}
@Injectable({
providedIn: 'root'
})
@@ -29,11 +52,15 @@ export class MarkdownService {
render(markdown: string, allNotes: Note[], currentNote?: Note): string {
const env: MarkdownRenderEnv = { codeBlockIndex: 0 };
const markdownIt = this.createMarkdownIt(env);
const headingSlugState = new Map<string, number>();
const markdownIt = this.createMarkdownIt(env, headingSlugState);
const preprocessed = this.decorateInlineTags(markdown);
let html = markdownIt.render(preprocessed, env);
const preprocessing = this.preprocessMarkdown(markdown);
const decorated = this.decorateInlineTags(preprocessing.markdown);
let html = markdownIt.render(decorated, env);
html = this.restoreWikiLinks(html, preprocessing.wikiLinks);
html = this.restoreMath(html, preprocessing.math);
html = this.transformCallouts(html);
html = this.transformTaskLists(html);
html = this.wrapTables(html);
@@ -62,10 +89,6 @@ export class MarkdownService {
</figure>`;
});
// Placeholder for unsupported elements
html = html.replace(/\$\$(.*?)\$\$/g, '<div class="my-4 p-4 bg-obs-l-bg-secondary text-obs-l-text-muted dark:bg-obs-d-bg-secondary dark:text-obs-d-text-muted rounded text-center">[LaTeX Equation: $1]</div>');
return html;
}
@@ -123,8 +146,8 @@ export class MarkdownService {
this.tagColorCache.set(normalized, index);
return `md-tag-color-${index}`;
}
private createMarkdownIt(env: MarkdownRenderEnv): MarkdownIt {
const md = new MarkdownIt('commonmark', {
private createMarkdownIt(env: MarkdownRenderEnv, slugState: Map<string, number>): MarkdownIt {
const md = new MarkdownIt({
html: true,
linkify: false,
typographer: false,
@@ -138,7 +161,7 @@ export class MarkdownService {
});
md.use(markdownItAnchor, {
slugify: (str) => this.slugify(str),
slugify: (str) => this.slugify(str, slugState),
tabIndex: false
});
@@ -146,11 +169,26 @@ export class MarkdownService {
enabled: false
});
md.use(markdownItMultimdTable, {
multiline: true,
rowspan: true,
headerless: true
});
md.enable(['table']);
md.use(markdownItFootnote);
md.renderer.rules.fence = (tokens, idx, _options, renderEnv) => {
const list = tokens as unknown as MarkdownItToken[];
return this.renderFence(list[idx], renderEnv as MarkdownRenderEnv);
};
md.renderer.rules.code_block = (tokens, idx, _options, renderEnv) => {
const list = tokens as unknown as MarkdownItToken[];
return this.renderFence(list[idx], renderEnv as MarkdownRenderEnv);
};
md.renderer.rules.code_inline = (tokens, idx) => {
const content = tokens[idx].content;
return `<code class="inline-code">${this.escapeHtml(content)}</code>`;
@@ -224,6 +262,22 @@ export class MarkdownService {
</figure>`;
};
md.renderer.rules.footnote_block_open = () => '<section class="md-footnotes text-sm text-obs-l-text-muted dark:text-obs-d-text-muted mt-12"><header class="font-semibold uppercase tracking-wide mb-2">Notes</header><ol class="space-y-2">';
md.renderer.rules.footnote_block_close = () => '</ol></section>';
const defaultFootnoteOpen = md.renderer.rules.footnote_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.footnote_open = (tokens, idx, options, renderEnv, self) => {
tokens[idx].attrJoin('class', 'md-footnote-item');
return defaultFootnoteOpen(tokens, idx, options, renderEnv, self);
};
md.renderer.rules.footnote_ref = (tokens, idx, options, renderEnv, self) => {
const list = tokens as unknown as MarkdownItToken[];
const token = list[idx];
const id = token.attrGet('id') ?? '';
return `<sup class="md-footnote-ref"><a href="#${this.escapeHtml(id)}" class="md-external-link" rel="footnote">${token.content}</a></sup>`;
};
md.renderer.rules.hr = () => '<hr class="my-6 border-obs-l-border dark:border-obs-d-border">';
return md;
@@ -249,7 +303,7 @@ export class MarkdownService {
</div>
`;
} else {
const highlightedCode = this.highlightCode(token.content, normalizedLanguage);
const highlightedCode = this.highlightCode(token.content, normalizedLanguage, rawLanguage);
bodyHtml = `<pre class="code-block__body"><code class="hljs" data-raw-code="${encodedRawCode}">${highlightedCode}</code></pre>`;
}
@@ -269,13 +323,133 @@ export class MarkdownService {
return `<div class="${wrapperClass}" style="max-width: 800px; width: 100%; margin: 0;" data-language="${languageLabel.toLowerCase()}" data-code-id="${codeId}">${headerHtml}${bodyHtml}</div>`;
}
private highlightCode(code: string, language: string): string {
if (language && hljs.getLanguage(language)) {
return hljs.highlight(code, { language }).value;
private highlightCode(code: string, normalizedLanguage: string, rawLanguage: string): string {
if (normalizedLanguage && hljs.getLanguage(normalizedLanguage)) {
return hljs.highlight(code, { language: normalizedLanguage }).value;
}
if (rawLanguage && !hljs.getLanguage(normalizedLanguage)) {
return this.escapeHtml(code);
}
return hljs.highlightAuto(code).value;
}
private preprocessMarkdown(input: string): PreprocessResult {
const wikiLinks: WikiLinkPlaceholder[] = [];
const math: MathPlaceholder[] = [];
let text = this.stripFrontmatter(input.replace(/\r\n/g, '\n'));
const wikiRegex = /(?<!\!)\[\[([^\]]+)\]\]/g;
text = text.replace(wikiRegex, (_match, inner) => {
const placeholder = `@@__WIKILINK_${wikiLinks.length}__@@`;
const [targetPartRaw, aliasRaw] = `${inner}`.split('|');
let targetPart = targetPartRaw ?? '';
let alias = aliasRaw ?? '';
let block: string | undefined;
let heading: string | undefined;
const blockIndex = targetPart.indexOf('^');
if (blockIndex >= 0) {
block = targetPart.slice(blockIndex + 1).trim();
targetPart = targetPart.slice(0, blockIndex);
}
const headingIndex = targetPart.indexOf('#');
if (headingIndex >= 0) {
heading = targetPart.slice(headingIndex + 1).trim();
targetPart = targetPart.slice(0, headingIndex);
}
const target = targetPart.trim();
const display = (alias || heading || block || target).trim() || 'Untitled';
const headingSlug = heading ? this.slugify(heading) : undefined;
wikiLinks.push({
placeholder,
alias: display,
target,
headingSlug,
headingText: heading,
block
});
return placeholder;
});
const addMathPlaceholder = (expression: string, display: 'block' | 'inline') => {
const placeholder = `@@__MATH_${display.toUpperCase()}_${math.length}__@@`;
math.push({ placeholder, expression: expression.trim(), display });
return placeholder;
};
text = text.replace(/(^|[^\\])\$\$([\s\S]+?)\$\$/g, (match, prefix, expr) => {
const placeholder = addMathPlaceholder(expr, 'block');
return `${prefix}${placeholder}`;
});
text = text.replace(/\\\[(.+?)\\\]/g, (_match, expr) => addMathPlaceholder(expr, 'block'));
text = text.replace(/(?<!\\)\$(?!\s)([^$]+?)(?<!\\)\$(?!\d)/g, (_match, expr) => addMathPlaceholder(expr, 'inline'));
text = text.replace(/\\\((.+?)\\\)/g, (_match, expr) => addMathPlaceholder(expr, 'inline'));
return { markdown: text, wikiLinks, math };
}
private restoreWikiLinks(html: string, links: WikiLinkPlaceholder[]): string {
if (!links.length) {
return html;
}
return links.reduce((acc, link) => {
const attrs: string[] = ['class="md-wiki-link"'];
if (link.target) {
attrs.push(`data-target="${this.escapeAttribute(link.target)}"`);
}
if (link.headingSlug) {
attrs.push(`data-heading="${this.escapeAttribute(link.headingSlug)}"`);
}
if (link.headingText) {
attrs.push(`data-heading-text="${this.escapeAttribute(link.headingText)}"`);
}
if (link.block) {
attrs.push(`data-block="${this.escapeAttribute(link.block)}"`);
}
const replacement = `<a ${attrs.join(' ')}>${this.escapeHtml(link.alias)}</a>`;
return acc.split(link.placeholder).join(replacement);
}, html);
}
private restoreMath(html: string, placeholders: MathPlaceholder[]): string {
if (!placeholders.length) {
return html;
}
return placeholders.reduce((acc, item) => {
const safeExpr = this.escapeHtml(item.expression);
const dataAttr = this.escapeAttribute(item.expression);
const replacement = item.display === 'block'
? `<div class="md-math-block" data-math="${dataAttr}">${safeExpr}</div>`
: `<span class="md-math-inline" data-math="${dataAttr}">${safeExpr}</span>`;
return acc.split(item.placeholder).join(replacement);
}, html);
}
private stripFrontmatter(markdown: string): string {
if (!markdown.startsWith('---')) {
return markdown;
}
const match = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*(\n|$)/);
if (!match) {
return markdown;
}
return markdown.slice(match[0].length);
}
private transformCallouts(html: string): string {
return html.replace(/<blockquote([\s\S]*?)>([\s\S]*?)<\/blockquote>/g, (match, _attrs, inner) => {
const firstParagraphMatch = inner.match(/^\s*<p>([\s\S]*?)<\/p>([\s\S]*)$/);
@@ -377,12 +551,39 @@ export class MarkdownService {
.replace(/'/g, "&#039;");
}
private slugify(text: string): string {
return text.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
private escapeAttribute(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
private slugify(text: string, state?: Map<string, number>): string {
let base = text
.toString()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.trim()
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.toLowerCase();
if (!base) {
base = 'section';
}
if (!state) {
return base;
}
let count = state.get(base);
if (count === undefined) {
count = 0;
}
state.set(base, count + 1);
return count === 0 ? base : `${base}-${count}`;
}
}