src / markdown.ts
/**
* Markdown -> HTML conversion using OneMore's bundled Markdig parser (ported
* from local-onenote-mcp's xml_utils.py). Requires the OneMore add-in, or the
* ONENOTE_MANAGER_MARKDIG_DLL environment variable pointing at Markdig.Signed.dll.
*
* DLL discovery is done inside PowerShell (env -> registry -> known paths) so
* Node needs no registry access.
*/
import { spawn } from "child_process";
import { randomUUID } from "crypto";
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const MARKDOWN_POWERSHELL = String.raw`
$ErrorActionPreference = "Stop"
function Resolve-MarkdigDll {
if (-not [string]::IsNullOrWhiteSpace($env:ONENOTE_MANAGER_MARKDIG_DLL) -and (Test-Path -LiteralPath $env:ONENOTE_MANAGER_MARKDIG_DLL)) {
return $env:ONENOTE_MANAGER_MARKDIG_DLL
}
$candidates = @()
try {
$appPath = (Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\App Paths\River.OneMoreAddIn.dll" -ErrorAction Stop).Path
if ($appPath) { $candidates += (Join-Path (Split-Path $appPath -Parent) "Markdig.Signed.dll") }
} catch {}
$candidates += "C:\Program Files\River\OneMoreAddIn\Markdig.Signed.dll"
$candidates += "C:\Program Files (x86)\River\OneMoreAddIn\Markdig.Signed.dll"
foreach ($c in $candidates) { if ($c -and (Test-Path -LiteralPath $c)) { return $c } }
throw "Markdown conversion requires OneMore's Markdig.Signed.dll. Install OneMore or set ONENOTE_MANAGER_MARKDIG_DLL."
}
$dll = Resolve-MarkdigDll
[Reflection.Assembly]::LoadFrom($dll) | Out-Null
$builder = [Markdig.MarkdownPipelineBuilder]::new()
[Markdig.MarkdownExtensions]::UseAdvancedExtensions($builder) | Out-Null
$pipeline = $builder.Build()
$markdown = Get-Content -LiteralPath $env:ONENOTE_MANAGER_MD_INPUT -Raw -Encoding UTF8
$writer = [System.IO.StringWriter]::new()
[Markdig.Markdown]::ToHtml($markdown, $writer, $pipeline, $null) | Out-Null
$html = $writer.ToString()
[System.IO.File]::WriteAllText($env:ONENOTE_MANAGER_MD_OUTPUT, $html, [System.Text.UTF8Encoding]::new($false))
`;
export async function markdownToHtml(content: string, timeoutSeconds = 30): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "onenote-manager-md-"));
const inputPath = join(dir, `${randomUUID()}.md`);
const outputPath = join(dir, `${randomUUID()}.html`);
try {
await writeFile(inputPath, content, "utf-8");
await new Promise<void>((resolve, reject) => {
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "-"], {
windowsHide: true,
env: {
...process.env,
ONENOTE_MANAGER_MD_INPUT: inputPath,
ONENOTE_MANAGER_MD_OUTPUT: outputPath,
},
});
let stderr = "";
const timer = setTimeout(() => {
child.kill();
reject(new Error(`Markdown conversion timed out after ${timeoutSeconds} seconds.`));
}, timeoutSeconds * 1000);
child.stderr?.on("data", (c: Buffer) => (stderr += c.toString("utf8")));
child.on("error", (e) => {
clearTimeout(timer);
reject(new Error(`Failed to launch PowerShell for Markdown: ${e.message}`));
});
child.on("close", (code) => {
clearTimeout(timer);
if (code !== 0) reject(new Error(stderr.trim() || "Markdown conversion failed."));
else resolve();
});
child.stdin?.write(MARKDOWN_POWERSHELL);
child.stdin?.end();
});
const html = await readFile(outputPath, "utf-8").catch(() => {
throw new Error("Markdown conversion did not write an HTML output file.");
});
return stripBom(html);
} finally {
await rm(dir, { recursive: true, force: true }).catch(() => {});
}
}
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}