Browse Source

Asciidoctor rendering of Asciimath, LaTeX, PlantUML, and SVG placeholders for BPMN and TikZ

master
Silberengel 8 months ago
parent
commit
bb14cdff5a
  1. 7
      package-lock.json
  2. 1
      package.json
  3. 22
      src/app.html
  4. 15
      src/lib/components/PublicationSection.svelte
  5. 1
      src/lib/components/cards/BlogHeader.svelte
  6. 42
      src/lib/parser.ts
  7. 74
      src/lib/utils/markup/MarkupInfo.md
  8. 311
      src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts
  9. 208
      src/lib/utils/markup/asciidoctorExtensions.ts
  10. 18
      src/lib/utils/markup/asciidoctorPostProcessor.ts
  11. 60
      src/lib/utils/markup/tikzRenderer.ts
  12. 5
      src/types/global.d.ts
  13. 5
      src/types/plantuml-encoder.d.ts
  14. 73
      test_data/AsciidocFiles/SimpleTest.adoc

7
package-lock.json generated

@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
"highlight.js": "^11.11.1",
"node-emoji": "^2.2.0",
"nostr-tools": "2.10.x",
"plantuml-encoder": "^1.4.0",
"qrcode": "^1.5.4"
},
"devDependencies": {
@ -5496,6 +5497,12 @@ @@ -5496,6 +5497,12 @@
"node": ">= 6"
}
},
"node_modules/plantuml-encoder": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/plantuml-encoder/-/plantuml-encoder-1.4.0.tgz",
"integrity": "sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==",
"license": "MIT"
},
"node_modules/playwright": {
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",

1
package.json

@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
"highlight.js": "^11.11.1",
"node-emoji": "^2.2.0",
"nostr-tools": "2.10.x",
"plantuml-encoder": "^1.4.0",
"qrcode": "^1.5.4"
},
"devDependencies": {

22
src/app.html

@ -4,6 +4,28 @@ @@ -4,6 +4,28 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png?v=2" />
<meta name="viewport" content="width=device-width" />
<!-- MathJax for math rendering -->
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<!-- highlight.js for code highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
%sveltekit.head%
</head>

15
src/lib/components/PublicationSection.svelte

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
<script lang='ts'>
console.log('PublicationSection loaded');
import type { PublicationTree } from "$lib/data_structures/publication_tree";
import { contentParagraph, sectionHeading } from "$lib/snippets/PublicationSnippets.svelte";
import { NDKEvent } from "@nostr-dev-kit/ndk";
@ -6,7 +7,7 @@ @@ -6,7 +7,7 @@
import { getContext } from "svelte";
import type { Asciidoctor, Document } from "asciidoctor";
import { getMatchingTags } from '$lib/utils/nostrUtils';
import { postProcessAsciidoctorHtml } from '$lib/utils/markup/asciidoctorPostProcessor';
import { postProcessAdvancedAsciidoctorHtml } from '$lib/utils/markup/advancedAsciidoctorPostProcessor';
let {
address,
@ -41,7 +42,7 @@ @@ -41,7 +42,7 @@
let leafContent: Promise<string | Document> = $derived.by(async () => {
const rawContent = (await leafEvent)?.content ?? '';
const asciidoctorHtml = asciidoctor.convert(rawContent);
return await postProcessAsciidoctorHtml(asciidoctorHtml.toString());
return await postProcessAdvancedAsciidoctorHtml(asciidoctorHtml.toString());
});
let previousLeafEvent: NDKEvent | null = $derived.by(() => {
@ -107,12 +108,20 @@ @@ -107,12 +108,20 @@
ref(sectionRef);
});
$effect(() => {
if (leafContent) {
console.log('leafContent HTML:', leafContent.toString());
}
});
</script>
<section id={address} bind:this={sectionRef} class='publication-leather content-visibility-auto'>
{#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])}
<TextPlaceholder size='xxl' />
{:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]}
{@const contentString = leafContent.toString()}
{@const _ = (() => { console.log('leafContent HTML:', contentString); return null; })()}
{#each divergingBranches as [branch, depth]}
{@render sectionHeading(getMatchingTags(branch, 'title')[0]?.[1] ?? '', depth)}
{/each}
@ -120,6 +129,6 @@ @@ -120,6 +129,6 @@
{@const leafDepth = leafHierarchy.length - 1}
{@render sectionHeading(leafTitle, leafDepth)}
{/if}
{@render contentParagraph(leafContent.toString(), publicationType ?? 'article', false)}
{@render contentParagraph(contentString, publicationType ?? 'article', false)}
{/await}
</section>

1
src/lib/components/cards/BlogHeader.svelte

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing";
import CardActions from "$components/util/CardActions.svelte";
import { getMatchingTags } from '$lib/utils/nostrUtils';
const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: string, event: NDKEvent, onBlogUpdate?: any, active: boolean }>();

42
src/lib/parser.ts

@ -150,6 +150,23 @@ export default class Pharos { @@ -150,6 +150,23 @@ export default class Pharos {
pharos.treeProcessor(treeProcessor, document);
});
});
// Add advanced extensions for math, PlantUML, BPMN, and TikZ
this.loadAdvancedExtensions();
}
/**
* Loads advanced extensions for math, PlantUML, BPMN, and TikZ rendering
*/
private async loadAdvancedExtensions(): Promise<void> {
try {
const { createAdvancedExtensions } = await import('./utils/markup/asciidoctorExtensions');
const advancedExtensions = createAdvancedExtensions();
// Note: Extensions merging might not be available in this version
// We'll handle this in the parse method instead
} catch (error) {
console.warn('Advanced extensions not available:', error);
}
}
parse(content: string, options?: ProcessorOptions | undefined): void {
@ -158,9 +175,15 @@ export default class Pharos { @@ -158,9 +175,15 @@ export default class Pharos {
content = ensureAsciiDocHeader(content);
try {
const mergedAttributes = Object.assign(
{},
options && typeof options.attributes === 'object' ? options.attributes : {},
{ 'source-highlighter': 'highlightjs' }
);
this.html = this.asciidoctor.convert(content, {
'extension_registry': this.pharosExtensions,
...options,
'extension_registry': this.pharosExtensions,
attributes: mergedAttributes,
}) as string | Document | undefined;
} catch (error) {
console.error(error);
@ -783,7 +806,7 @@ export default class Pharos { @@ -783,7 +806,7 @@ export default class Pharos {
authors: document
.getAuthors()
.map(author => author.getName())
.filter(name => name != null),
.filter((name): name is string => name != null),
version: document.getRevisionNumber(),
edition: document.getRevisionRemark(),
publicationDate: document.getRevisionDate(),
@ -794,13 +817,14 @@ export default class Pharos { @@ -794,13 +817,14 @@ export default class Pharos {
}
if (this.rootIndexMetadata.version || this.rootIndexMetadata.edition) {
event.tags.push(
[
'version',
this.rootIndexMetadata.version!,
this.rootIndexMetadata.edition!
].filter(value => value != null)
);
const versionTags: string[] = ['version'];
if (this.rootIndexMetadata.version) {
versionTags.push(this.rootIndexMetadata.version);
}
if (this.rootIndexMetadata.edition) {
versionTags.push(this.rootIndexMetadata.edition);
}
event.tags.push(versionTags);
}
if (this.rootIndexMetadata.publicationDate) {

74
src/lib/utils/markup/MarkupInfo.md

@ -42,8 +42,82 @@ AsciiDoc supports a much broader set of formatting, semantic, and structural fea @@ -42,8 +42,82 @@ AsciiDoc supports a much broader set of formatting, semantic, and structural fea
- Advanced tables, callouts, admonitions
- Cross-references, footnotes, and bibliography
- Custom attributes and macros
- **Math rendering** (Asciimath and LaTeX)
- **Diagram rendering** (PlantUML, BPMN, TikZ)
- And much more
### Advanced Content Types
Alexandria supports rendering of advanced content types commonly used in academic, technical, and business documents:
#### Math Rendering
Use `[stem]` blocks for mathematical expressions:
```asciidoc
[stem]
++++
\frac{\partial f}{\partial x} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}
++++
```
Inline math is also supported using `$...$` or `\(...\)` syntax.
#### PlantUML Diagrams
PlantUML diagrams are automatically detected and rendered:
```asciidoc
[source,plantuml]
----
@startuml
participant User
participant System
User -> System: Login Request
System --> User: Login Response
@enduml
----
```
#### BPMN Diagrams
BPMN (Business Process Model and Notation) diagrams are supported:
```asciidoc
[source,bpmn]
----
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
<bpmn:process id="Process_1">
<bpmn:startEvent id="StartEvent_1" name="Start"/>
<bpmn:task id="Task_1" name="Process Task"/>
<bpmn:endEvent id="EndEvent_1" name="End"/>
</bpmn:process>
</bpmn:definitions>
----
```
#### TikZ Diagrams
TikZ diagrams for mathematical illustrations:
```asciidoc
[source,tikz]
----
\begin{tikzpicture}
\draw[thick,red] (0,0) circle (1cm);
\draw[thick,blue] (2,0) rectangle (3,1);
\end{tikzpicture}
----
```
### Rendering Features
- **Automatic Detection**: Content types are automatically detected based on syntax
- **Fallback Display**: If rendering fails, the original source code is displayed
- **Source Code**: Click "Show source" to view the original code
- **Responsive Design**: All rendered content is responsive and works on mobile devices
For more information on AsciiDoc, see the [AsciiDoc documentation](https://asciidoc.org/).
---

311
src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts

@ -0,0 +1,311 @@ @@ -0,0 +1,311 @@
import { postProcessAsciidoctorHtml } from './asciidoctorPostProcessor';
import plantumlEncoder from 'plantuml-encoder';
/**
* Unified post-processor for Asciidoctor HTML that handles:
* - Math rendering (Asciimath/Latex, stem blocks)
* - PlantUML diagrams
* - BPMN diagrams
* - TikZ diagrams
*/
export async function postProcessAdvancedAsciidoctorHtml(html: string): Promise<string> {
if (!html) return html;
try {
// First apply the basic post-processing (wikilinks, nostr addresses)
let processedHtml = await postProcessAsciidoctorHtml(html);
// Unified math block processing
processedHtml = fixAllMathBlocks(processedHtml);
// Process PlantUML blocks
processedHtml = processPlantUMLBlocks(processedHtml);
// Process BPMN blocks
processedHtml = processBPMNBlocks(processedHtml);
// Process TikZ blocks
processedHtml = processTikZBlocks(processedHtml);
// After all processing, apply highlight.js if available
if (typeof window !== 'undefined' && typeof window.hljs?.highlightAll === 'function') {
setTimeout(() => window.hljs!.highlightAll(), 0);
}
if (typeof window !== 'undefined' && typeof (window as any).MathJax?.typesetPromise === 'function') {
setTimeout(() => (window as any).MathJax.typesetPromise(), 0);
}
return processedHtml;
} catch (error) {
console.error('Error in postProcessAdvancedAsciidoctorHtml:', error);
return html; // Return original HTML if processing fails
}
}
/**
* Fixes all math blocks for MathJax rendering.
* Handles stem blocks, inline math, and normalizes delimiters.
*/
function fixAllMathBlocks(html: string): string {
// Unescape \$ to $ for math delimiters
html = html.replace(/\\\$/g, '$');
// DEBUG: Log the HTML before MathJax runs
if (html.includes('latexmath')) {
console.debug('Processed HTML for latexmath:', html);
}
// Block math: <div class="stemblock"><div class="content">...</div></div>
html = html.replace(
/<div class="stemblock">\s*<div class="content">([\s\S]*?)<\/div>\s*<\/div>/g,
(_match, mathContent) => {
// DEBUG: Log the original and cleaned math content
console.debug('Block math original:', mathContent);
console.debug('Block math char codes:', Array.from(mathContent as string).map((c: string) => c.charCodeAt(0)));
let cleanMath = mathContent
.replace(/<span>\$<\/span>/g, '')
.replace(/<span>\$\$<\/span>/g, '')
// Remove $ or $$ on their own line, or surrounded by whitespace/newlines
.replace(/(^|[\n\r\s])\$([\n\r\s]|$)/g, '$1$2')
.replace(/(^|[\n\r\s])\$\$([\n\r\s]|$)/g, '$1$2')
// Remove all leading and trailing whitespace and $
.replace(/^[\s$]+/, '').replace(/[\s$]+$/, '')
.trim(); // Final trim to remove any stray whitespace or $
console.debug('Block math cleaned:', cleanMath);
console.debug('Block math cleaned char codes:', Array.from(cleanMath as string).map((c: string) => c.charCodeAt(0)));
// Always wrap in $$...$$
return `<div class="stemblock"><div class="content">$$${cleanMath}$$</div></div>`;
}
);
// Inline math: <span>$</span> ... <span>$</span> (allow whitespace/newlines)
html = html.replace(
/<span>\$<\/span>\s*([\s\S]+?)\s*<span>\$<\/span>/g,
(_match, mathContent) => `<span class="math-inline">$${mathContent.trim()}$</span>`
);
// Inline math: stem:[...] or latexmath:[...]
html = html.replace(
/stem:\[([^\]]+?)\]/g,
(_match, content) => `<span class="math-inline">$${content.trim()}$</span>`
);
html = html.replace(
/latexmath:\[([^\]]+?)\]/g,
(_match, content) => `<span class="math-inline">\\(${content.trim().replace(/\\\\/g, '\\')}\\)</span>`
);
html = html.replace(
/asciimath:\[([^\]]+?)\]/g,
(_match, content) => `<span class="math-inline">\`${content.trim()}\`</span>`
);
return html;
}
/**
* Processes PlantUML blocks in HTML content
*/
function processPlantUMLBlocks(html: string): string {
// Only match code blocks with class 'language-plantuml' or 'plantuml'
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre class="highlight">\s*<code[^>]*class="[^"]*(?:language-plantuml|plantuml)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
try {
// Unescape HTML for PlantUML server, but escape for <code>
const rawContent = decodeHTMLEntities(content);
const encoded = plantumlEncoder.encode(rawContent);
const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`;
return `<div class="plantuml-block my-4">
<img src="${plantUMLUrl}" alt="PlantUML diagram"
class="plantuml-diagram max-w-full h-auto rounded-lg shadow-lg"
loading="lazy" decoding="async">
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show PlantUML source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(rawContent)}</code>
</pre>
</details>
</div>`;
} catch (error) {
console.warn('Failed to process PlantUML block:', error);
return match;
}
}
);
// Fallback: match <pre> blocks whose content starts with @startuml or @start (global, robust)
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre>([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
const lines = content.trim().split('\n');
if (lines[0].trim().startsWith('@startuml') || lines[0].trim().startsWith('@start')) {
try {
const rawContent = decodeHTMLEntities(content);
const encoded = plantumlEncoder.encode(rawContent);
const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`;
return `<div class="plantuml-block my-4">
<img src="${plantUMLUrl}" alt="PlantUML diagram"
class="plantuml-diagram max-w-full h-auto rounded-lg shadow-lg"
loading="lazy" decoding="async">
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show PlantUML source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(rawContent)}</code>
</pre>
</details>
</div>`;
} catch (error) {
console.warn('Failed to process PlantUML fallback block:', error);
return match;
}
}
return match;
}
);
return html;
}
function decodeHTMLEntities(text: string): string {
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
/**
* Processes BPMN blocks in HTML content
*/
function processBPMNBlocks(html: string): string {
// Only match code blocks with class 'language-bpmn' or 'bpmn'
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre class="highlight">\s*<code[^>]*class="[^"]*(?:language-bpmn|bpmn)[^\"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
try {
return `<div class="bpmn-block my-4">
<div class="bpmn-diagram p-4 bg-blue-50 dark:bg-blue-900 rounded-lg border border-blue-200 dark:border-blue-700">
<div class="text-center text-blue-600 dark:text-blue-400 mb-2">
<svg class="inline w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
BPMN Diagram
</div>
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show BPMN source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(content)}</code>
</pre>
</details>
</div>
</div>`;
} catch (error) {
console.warn('Failed to process BPMN block:', error);
return match;
}
}
);
// Fallback: match <pre> blocks whose content contains 'bpmn:' or '<?xml' and 'bpmn'
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre>([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
const text = content.trim();
if (text.includes('bpmn:') || (text.startsWith('<?xml') && text.includes('bpmn'))) {
try {
return `<div class="bpmn-block my-4">
<div class="bpmn-diagram p-4 bg-blue-50 dark:bg-blue-900 rounded-lg border border-blue-200 dark:border-blue-700">
<div class="text-center text-blue-600 dark:text-blue-400 mb-2">
<svg class="inline w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
BPMN Diagram
</div>
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show BPMN source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(content)}</code>
</pre>
</details>
</div>
</div>`;
} catch (error) {
console.warn('Failed to process BPMN fallback block:', error);
return match;
}
}
return match;
}
);
return html;
}
/**
* Processes TikZ blocks in HTML content
*/
function processTikZBlocks(html: string): string {
// Only match code blocks with class 'language-tikz' or 'tikz'
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre class="highlight">\s*<code[^>]*class="[^"]*(?:language-tikz|tikz)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
try {
return `<div class="tikz-block my-4">
<div class="tikz-diagram p-4 bg-green-50 dark:bg-green-900 rounded-lg border border-green-200 dark:border-green-700">
<div class="text-center text-green-600 dark:text-green-400 mb-2">
<svg class="inline w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
</svg>
TikZ Diagram
</div>
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show TikZ source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(content)}</code>
</pre>
</details>
</div>
</div>`;
} catch (error) {
console.warn('Failed to process TikZ block:', error);
return match;
}
}
);
// Fallback: match <pre> blocks whose content starts with \begin{tikzpicture} or contains tikz
html = html.replace(
/<div class="listingblock">\s*<div class="content">\s*<pre>([\s\S]*?)<\/pre>\s*<\/div>\s*<\/div>/g,
(match, content) => {
const lines = content.trim().split('\n');
if (lines[0].trim().startsWith('\\begin{tikzpicture}') || content.includes('tikz')) {
try {
return `<div class="tikz-block my-4">
<div class="tikz-diagram p-4 bg-green-50 dark:bg-green-900 rounded-lg border border-green-200 dark:border-green-700">
<div class="text-center text-green-600 dark:text-green-400 mb-2">
<svg class="inline w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
</svg>
TikZ Diagram
</div>
<details class="mt-2">
<summary class="cursor-pointer text-sm text-gray-600 dark:text-gray-400">
Show TikZ source
</summary>
<pre class="mt-2 p-2 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-x-auto">
<code>${escapeHtml(content)}</code>
</pre>
</details>
</div>
</div>`;
} catch (error) {
console.warn('Failed to process TikZ fallback block:', error);
return match;
}
}
return match;
}
);
return html;
}
/**
* Escapes HTML characters for safe display
*/
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

208
src/lib/utils/markup/asciidoctorExtensions.ts

@ -0,0 +1,208 @@ @@ -0,0 +1,208 @@
import { renderTikZ } from './tikzRenderer';
import asciidoctor from 'asciidoctor';
// Simple math rendering using MathJax CDN
function renderMath(content: string): string {
return `<div class="math-block" data-math="${encodeURIComponent(content)}">
<div class="math-content">${content}</div>
<script>
if (typeof MathJax !== 'undefined') {
MathJax.typesetPromise([document.querySelector('.math-content')]);
}
</script>
</div>`;
}
// Simple PlantUML rendering using PlantUML server
function renderPlantUML(content: string): string {
// Encode content for PlantUML server
const encoded = btoa(unescape(encodeURIComponent(content)));
const plantUMLUrl = `https://www.plantuml.com/plantuml/svg/${encoded}`;
return `<img src="${plantUMLUrl}" alt="PlantUML diagram" class="plantuml-diagram max-w-full h-auto rounded-lg shadow-lg my-4" loading="lazy" decoding="async">`;
}
/**
* Creates Asciidoctor extensions for advanced content rendering
* including Asciimath/Latex, PlantUML, BPMN, and TikZ
*/
export function createAdvancedExtensions(): any {
const Asciidoctor = asciidoctor();
const extensions = Asciidoctor.Extensions.create();
// Math rendering extension (Asciimath/Latex)
extensions.treeProcessor(function (this: any) {
const dsl = this;
dsl.process(function (this: any, document: any) {
const treeProcessor = this;
processMathBlocks(treeProcessor, document);
});
});
// PlantUML rendering extension
extensions.treeProcessor(function (this: any) {
const dsl = this;
dsl.process(function (this: any, document: any) {
const treeProcessor = this;
processPlantUMLBlocks(treeProcessor, document);
});
});
// TikZ rendering extension
extensions.treeProcessor(function (this: any) {
const dsl = this;
dsl.process(function (this: any, document: any) {
const treeProcessor = this;
processTikZBlocks(treeProcessor, document);
});
});
// --- NEW: Support [plantuml], [tikz], [bpmn] as source blocks ---
// Helper to register a block for a given name and treat it as a source block
function registerDiagramBlock(name: string) {
extensions.block(name, function (this: any) {
const self = this;
self.process(function (parent: any, reader: any, attrs: any) {
// Read the block content
const lines = reader.getLines();
// Create a source block with the correct language and lang attributes
const block = self.createBlock(parent, 'source', lines, {
...attrs,
language: name,
lang: name,
style: 'source',
role: name,
});
block.setAttribute('language', name);
block.setAttribute('lang', name);
block.setAttribute('style', 'source');
block.setAttribute('role', name);
block.setOption('source', true);
block.setOption('listing', true);
block.setStyle('source');
return block;
});
});
}
registerDiagramBlock('plantuml');
registerDiagramBlock('tikz');
registerDiagramBlock('bpmn');
// --- END NEW ---
return extensions;
}
/**
* Processes math blocks (stem blocks) and converts them to rendered HTML
*/
function processMathBlocks(treeProcessor: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {
if (block.getContext() === 'stem') {
const content = block.getContent();
if (content) {
try {
// Output as a single div with delimiters for MathJax
const rendered = `<div class="math-block">$$${content}$$</div>`;
block.setContent(rendered);
} catch (error) {
console.warn('Failed to render math:', error);
}
}
}
// Inline math: context 'inline' and style 'stem' or 'latexmath'
if (block.getContext() === 'inline' && (block.getStyle() === 'stem' || block.getStyle() === 'latexmath')) {
const content = block.getContent();
if (content) {
try {
const rendered = `<span class="math-inline">$${content}$</span>`;
block.setContent(rendered);
} catch (error) {
console.warn('Failed to render inline math:', error);
}
}
}
}
}
/**
* Processes PlantUML blocks and converts them to rendered SVG
*/
function processPlantUMLBlocks(treeProcessor: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {
if (block.getContext() === 'listing' && isPlantUMLBlock(block)) {
const content = block.getContent();
if (content) {
try {
// Use simple PlantUML rendering
const rendered = renderPlantUML(content);
// Replace the block content with the image
block.setContent(rendered);
} catch (error) {
console.warn('Failed to render PlantUML:', error);
// Keep original content if rendering fails
}
}
}
}
}
/**
* Processes TikZ blocks and converts them to rendered SVG
*/
function processTikZBlocks(treeProcessor: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {
if (block.getContext() === 'listing' && isTikZBlock(block)) {
const content = block.getContent();
if (content) {
try {
// Render TikZ to SVG
const svg = renderTikZ(content);
// Replace the block content with the SVG
block.setContent(svg);
} catch (error) {
console.warn('Failed to render TikZ:', error);
// Keep original content if rendering fails
}
}
}
}
}
/**
* Checks if a block contains PlantUML content
*/
function isPlantUMLBlock(block: any): boolean {
const content = block.getContent() || '';
const lines = content.split('\n');
// Check for PlantUML indicators
return lines.some((line: string) =>
line.trim().startsWith('@startuml') ||
line.trim().startsWith('@start') ||
line.includes('plantuml') ||
line.includes('uml')
);
}
/**
* Checks if a block contains TikZ content
*/
function isTikZBlock(block: any): boolean {
const content = block.getContent() || '';
const lines = content.split('\n');
// Check for TikZ indicators
return lines.some((line: string) =>
line.trim().startsWith('\\begin{tikzpicture}') ||
line.trim().startsWith('\\tikz') ||
line.includes('tikzpicture') ||
line.includes('tikz')
);
}

18
src/lib/utils/markup/asciidoctorPostProcessor.ts

@ -74,6 +74,23 @@ async function processNostrAddresses(html: string): Promise<string> { @@ -74,6 +74,23 @@ async function processNostrAddresses(html: string): Promise<string> {
return processedHtml;
}
/**
* Fixes AsciiDoctor stem blocks for MathJax rendering.
* Joins split spans and wraps content in $$...$$ for block math.
*/
function fixStemBlocks(html: string): string {
// Replace <div class="stemblock"><div class="content"><span>$</span>...<span>$</span></div></div>
// with <div class="stemblock"><div class="content">$$...$$</div></div>
return html.replace(
/<div class="stemblock">\s*<div class="content">\s*<span>\$<\/span>([\s\S]*?)<span>\$<\/span>\s*<\/div>\s*<\/div>/g,
(_match, mathContent) => {
// Remove any extra tags inside mathContent
const cleanMath = mathContent.replace(/<\/?span[^>]*>/g, '').trim();
return `<div class="stemblock"><div class="content">$$${cleanMath}$$</div></div>`;
}
);
}
/**
* Post-processes asciidoctor HTML output to add wikilink and nostr address rendering.
* This function should be called after asciidoctor.convert() to enhance the HTML output.
@ -87,6 +104,7 @@ export async function postProcessAsciidoctorHtml(html: string): Promise<string> @@ -87,6 +104,7 @@ export async function postProcessAsciidoctorHtml(html: string): Promise<string>
// Then process nostr addresses (but not those already in links)
processedHtml = await processNostrAddresses(processedHtml);
processedHtml = fixStemBlocks(processedHtml); // Fix math blocks for MathJax
return processedHtml;
} catch (error) {

60
src/lib/utils/markup/tikzRenderer.ts

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/**
* TikZ renderer using node-tikzjax
* Converts TikZ LaTeX code to SVG for browser rendering
*/
// We'll use a simple approach for now since node-tikzjax might not be available
// This is a placeholder implementation that can be enhanced later
export function renderTikZ(tikzCode: string): string {
try {
// For now, we'll create a simple SVG placeholder
// In a full implementation, this would use node-tikzjax or similar library
// Extract TikZ content and create a basic SVG
const svgContent = createBasicSVG(tikzCode);
return svgContent;
} catch (error) {
console.error('Failed to render TikZ:', error);
return `<div class="tikz-error text-red-500 p-4 border border-red-300 rounded">
<p class="font-bold">TikZ Rendering Error</p>
<p class="text-sm">Failed to render TikZ diagram. Original code:</p>
<pre class="mt-2 p-2 bg-gray-100 rounded text-xs overflow-x-auto">${tikzCode}</pre>
</div>`;
}
}
/**
* Creates a basic SVG placeholder for TikZ content
* This is a temporary implementation until proper TikZ rendering is available
*/
function createBasicSVG(tikzCode: string): string {
// Create a simple SVG with the TikZ code as text
const width = 400;
const height = 300;
return `<svg width="${width}" height="${height}" class="tikz-diagram max-w-full h-auto rounded-lg shadow-lg my-4" viewBox="0 0 ${width} ${height}">
<rect width="${width}" height="${height}" fill="white" stroke="#ccc" stroke-width="1"/>
<text x="10" y="20" font-family="monospace" font-size="12" fill="#666">
TikZ Diagram
</text>
<text x="10" y="40" font-family="monospace" font-size="10" fill="#999">
(Rendering not yet implemented)
</text>
<foreignObject x="10" y="60" width="${width - 20}" height="${height - 70}">
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: monospace; font-size: 10px; color: #666; overflow: hidden;">
<pre style="margin: 0; white-space: pre-wrap; word-break: break-all;">${escapeHtml(tikzCode)}</pre>
</div>
</foreignObject>
</svg>`;
}
/**
* Escapes HTML characters for safe display
*/
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

5
src/types/global.d.ts vendored

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
interface Window {
hljs?: {
highlightAll: () => void;
};
}

5
src/types/plantuml-encoder.d.ts vendored

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
declare module 'plantuml-encoder' {
export function encode(text: string): string;
const _default: { encode: typeof encode };
export default _default;
}

73
test_data/AsciidocFiles/SimpleTest.adoc

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
= Simple Advanced Rendering Test
This is a simple test document to verify that Alexandria's advanced rendering features are working correctly.
== Math Test
Here's a simple math expression:
[stem]
++++
E = mc^2
++++
And a more complex one:
[stem]
++++
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
++++
== PlantUML Test
A simple sequence diagram:
[source,plantuml]
----
@startuml
participant User
participant System
User -> System: Hello
System --> User: Hi there!
@enduml
----
== BPMN Test
A simple BPMN process:
[source,bpmn]
----
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
<bpmn:process id="Process_1">
<bpmn:startEvent id="StartEvent_1" name="Start"/>
<bpmn:task id="Task_1" name="Test Task"/>
<bpmn:endEvent id="EndEvent_1" name="End"/>
<bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="Task_1"/>
<bpmn:sequenceFlow id="Flow_2" sourceRef="Task_1" targetRef="EndEvent_1"/>
</bpmn:process>
</bpmn:definitions>
----
== TikZ Test
A simple TikZ diagram:
[source,tikz]
----
\begin{tikzpicture}
\draw[thick,red] (0,0) circle (1cm);
\draw[thick,blue] (2,0) rectangle (3,1);
\end{tikzpicture}
----
== Conclusion
If you can see:
1. Rendered math expressions
2. A PlantUML diagram
3. A BPMN diagram placeholder with source
4. A TikZ diagram placeholder with source
Then the advanced rendering is working correctly!
Loading…
Cancel
Save