mirror of
https://github.com/r-ca/md2pdf-meow.git
synced 2025-12-03 12:40:47 +00:00
118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
import MarkdownIt from 'markdown-it';
|
|
import implicitFigures from 'markdown-it-implicit-figures';
|
|
import namedHeaders from 'markdown-it-named-headers';
|
|
import anchor from 'anchor-markdown-header';
|
|
import path from 'node:path';
|
|
|
|
import { DOCUMENTS_DIR } from '../config.js';
|
|
import { logger } from '../logger.js';
|
|
import { readText, writeText } from '../utils/fs.js';
|
|
import { AssetManager } from './assetManager.js';
|
|
import { FILE_MARKER_PREFIX, FILE_MARKER_SUFFIX } from './fileMarkers.js';
|
|
|
|
/**
|
|
* 元々の scripts/mdit.js (2SC1815J/md2pdf, MIT License) を TypeScript へ移植したレンダラー。
|
|
* スラッグ生成ロジックはフォーク元の実装をベースにしている。
|
|
*/
|
|
function createRenderer(assetManager: AssetManager) {
|
|
const headerInstances: Record<string, number> = {};
|
|
|
|
const renderer = new MarkdownIt({ html: true })
|
|
.use(namedHeaders, {
|
|
slugify(header: string) {
|
|
if (headerInstances[header] !== undefined) {
|
|
headerInstances[header]++;
|
|
} else {
|
|
headerInstances[header] = 0;
|
|
}
|
|
const slug = anchor(header, 'github.com', headerInstances[header]);
|
|
const match = slug.match(/]\(#(.+?)\)$/);
|
|
return match ? decodeURI(match[1]) : header;
|
|
}
|
|
})
|
|
.use(implicitFigures, {
|
|
figcaption: true
|
|
});
|
|
|
|
const defaultImageRule = renderer.renderer.rules.image;
|
|
renderer.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
const token = tokens[idx];
|
|
const src = token.attrGet('src');
|
|
const sourceFile = (env as { currentFile?: string }).currentFile;
|
|
if (src && sourceFile) {
|
|
const resolved = assetManager.resolve(sourceFile, src);
|
|
if (resolved) {
|
|
token.attrSet('src', resolved);
|
|
}
|
|
}
|
|
if (defaultImageRule) {
|
|
return defaultImageRule(tokens, idx, options, env, self);
|
|
}
|
|
return self.renderToken(tokens, idx, options);
|
|
};
|
|
|
|
return renderer;
|
|
}
|
|
|
|
interface MarkdownSegment {
|
|
filePath: string;
|
|
content: string;
|
|
}
|
|
|
|
function toAbsoluteDocumentPath(relativePath: string) {
|
|
return path.resolve(DOCUMENTS_DIR, relativePath.trim());
|
|
}
|
|
|
|
function splitByFileMarkers(markdown: string, defaultFile?: string): MarkdownSegment[] {
|
|
const segments: MarkdownSegment[] = [];
|
|
const markerRegex = new RegExp(`${FILE_MARKER_PREFIX}(.*?)${FILE_MARKER_SUFFIX}`, 'g');
|
|
let currentFile = defaultFile;
|
|
let lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
|
|
while ((match = markerRegex.exec(markdown)) !== null) {
|
|
const chunk = markdown.slice(lastIndex, match.index);
|
|
if (chunk && currentFile) {
|
|
segments.push({ filePath: currentFile, content: chunk });
|
|
}
|
|
currentFile = toAbsoluteDocumentPath(match[1]);
|
|
lastIndex = markerRegex.lastIndex;
|
|
}
|
|
|
|
const tail = markdown.slice(lastIndex);
|
|
if (tail && currentFile) {
|
|
segments.push({ filePath: currentFile, content: tail });
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
export async function renderMarkdownToHtml(
|
|
markdownPath: string,
|
|
outputPath: string,
|
|
assetManager: AssetManager,
|
|
orderedFiles: string[]
|
|
) {
|
|
logger.info('Markdown から HTML へ変換しています…');
|
|
const markdown = await readText(markdownPath);
|
|
const renderer = createRenderer(assetManager);
|
|
const defaultFile = orderedFiles[0];
|
|
const segments = splitByFileMarkers(markdown, defaultFile);
|
|
|
|
if (segments.length === 0) {
|
|
const html = renderer.render(markdown);
|
|
await writeText(outputPath, html);
|
|
logger.succ(`${path.relative('.', outputPath)} を生成しました`);
|
|
return;
|
|
}
|
|
|
|
const htmlParts: string[] = [];
|
|
for (const segment of segments) {
|
|
htmlParts.push(renderer.render(segment.content, { currentFile: segment.filePath }));
|
|
}
|
|
|
|
const html = htmlParts.join('');
|
|
await writeText(outputPath, html);
|
|
logger.succ(`${path.relative('.', outputPath)} を生成しました`);
|
|
}
|