mirror of
https://github.com/r-ca/md2pdf-meow.git
synced 2025-12-03 12:40:47 +00:00
feat: support yaml-based template meta
This commit is contained in:
parent
bfe2e92245
commit
8a65b03780
19
README.md
19
README.md
@ -15,5 +15,24 @@ TypeScript ベースの Markdown → HTML → PDF 変換パイプラインです
|
|||||||
- `documents/` 配下の Markdown からは相対パスで画像を参照でき、ビルド時に `work/assets/` へ自動コピーしたうえで HTML / PDF に反映します。外部 URL やデータ URI もそのまま扱えます。
|
- `documents/` 配下の Markdown からは相対パスで画像を参照でき、ビルド時に `work/assets/` へ自動コピーしたうえで HTML / PDF に反映します。外部 URL やデータ URI もそのまま扱えます。
|
||||||
- PDF 生成は `puppeteer-html-pdf` を使い、Chrome 不在時には明示的に検出するようになっています。
|
- PDF 生成は `puppeteer-html-pdf` を使い、Chrome 不在時には明示的に検出するようになっています。
|
||||||
|
|
||||||
|
### メタ情報のカスタマイズ
|
||||||
|
- テンプレート固有のタイトル・著者・発行日などは `documents/_meta.yaml`(または `_meta.yml`)に記述します(存在しない場合はテンプレートのデフォルト値を使用)。
|
||||||
|
- 例:
|
||||||
|
```yaml
|
||||||
|
title: md2pdf-meow サンプル
|
||||||
|
author: ろむねこ
|
||||||
|
published: 2024/07/01
|
||||||
|
description: モバイルオーダー用ドキュメント
|
||||||
|
copyright: © 2024 rca
|
||||||
|
frontCover:
|
||||||
|
title: md2pdf-meow
|
||||||
|
published: 2024/07/01
|
||||||
|
backCover:
|
||||||
|
title: md2pdf-meow
|
||||||
|
pubDate: 2024/07/01
|
||||||
|
copyright: rca
|
||||||
|
```
|
||||||
|
- `frontCover` / `backCover` の各値は任意で、未指定時には `title` や `published` などの共通値が利用されます。
|
||||||
|
|
||||||
## 移植元
|
## 移植元
|
||||||
- https://github.com/2SC1815J/md2pdf
|
- https://github.com/2SC1815J/md2pdf
|
||||||
|
|||||||
13
documents/_meta.yaml
Normal file
13
documents/_meta.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
title: Template
|
||||||
|
author: ろむねこ
|
||||||
|
published: 0000/000/00
|
||||||
|
description: ""
|
||||||
|
copyright: template
|
||||||
|
frontCover:
|
||||||
|
title: Template
|
||||||
|
author: ろむねこ
|
||||||
|
published: 0000/000/00
|
||||||
|
backCover:
|
||||||
|
title: Template
|
||||||
|
pubDate: 0000/000/00
|
||||||
|
copyright: template
|
||||||
27
package-lock.json
generated
27
package-lock.json
generated
@ -10,9 +10,11 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chalk": "^2.2.4",
|
"@types/chalk": "^2.2.4",
|
||||||
|
"@types/yaml": "^1.9.7",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"puppeteer-html-pdf": "^4.0.8",
|
"puppeteer-html-pdf": "^4.0.8",
|
||||||
"tsx": "^4.17.0"
|
"tsx": "^4.17.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"anchor-markdown-header": "^0.5.7",
|
"anchor-markdown-header": "^0.5.7",
|
||||||
@ -568,6 +570,15 @@
|
|||||||
"undici-types": "~6.13.0"
|
"undici-types": "~6.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/yaml": {
|
||||||
|
"version": "1.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yaml/-/yaml-1.9.7.tgz",
|
||||||
|
"integrity": "sha512-8WMXRDD1D+wCohjfslHDgICd2JtMATZU8CkhH8LVJqcJs6dyYj5TGptzP8wApbmEullGBSsCEzzap73DQ1HJaA==",
|
||||||
|
"deprecated": "This is a stub types definition. yaml provides its own type definitions, so you do not need this installed.",
|
||||||
|
"dependencies": {
|
||||||
|
"yaml": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
@ -5486,6 +5497,20 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
@ -23,8 +23,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chalk": "^2.2.4",
|
"@types/chalk": "^2.2.4",
|
||||||
|
"@types/yaml": "^1.9.7",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"puppeteer-html-pdf": "^4.0.8",
|
"puppeteer-html-pdf": "^4.0.8",
|
||||||
"tsx": "^4.17.0"
|
"tsx": "^4.17.0",
|
||||||
|
"yaml": "^2.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { prepareWorkspace } from './pipeline/prepareWorkspace.js';
|
|||||||
import { AssetManager } from './pipeline/assetManager.js';
|
import { AssetManager } from './pipeline/assetManager.js';
|
||||||
import { renderMarkdownToHtml } from './pipeline/renderMarkdown.js';
|
import { renderMarkdownToHtml } from './pipeline/renderMarkdown.js';
|
||||||
import { renderTemplate } from './pipeline/renderTemplate.js';
|
import { renderTemplate } from './pipeline/renderTemplate.js';
|
||||||
|
import { loadMeta } from './pipeline/loadMeta.js';
|
||||||
|
|
||||||
async function runPipeline() {
|
async function runPipeline() {
|
||||||
logger.info('md2pdf-meow のビルドを開始します。');
|
logger.info('md2pdf-meow のビルドを開始します。');
|
||||||
@ -16,7 +17,8 @@ async function runPipeline() {
|
|||||||
const { outputPath: markdownPath, orderedFiles } = await concatMarkdown(assetManager);
|
const { outputPath: markdownPath, orderedFiles } = await concatMarkdown(assetManager);
|
||||||
await injectToc(markdownPath);
|
await injectToc(markdownPath);
|
||||||
await renderMarkdownToHtml(markdownPath, WORK_MARKDOWN_HTML, assetManager, orderedFiles);
|
await renderMarkdownToHtml(markdownPath, WORK_MARKDOWN_HTML, assetManager, orderedFiles);
|
||||||
await renderTemplate(TEMPLATE_FILE, WORK_TEMPLATE_HTML);
|
const meta = await loadMeta();
|
||||||
|
await renderTemplate(TEMPLATE_FILE, WORK_TEMPLATE_HTML, meta);
|
||||||
await inlineAssets(WORK_TEMPLATE_HTML, DIST_INLINE_HTML);
|
await inlineAssets(WORK_TEMPLATE_HTML, DIST_INLINE_HTML);
|
||||||
await generatePdf(DIST_INLINE_HTML, DIST_PDF);
|
await generatePdf(DIST_INLINE_HTML, DIST_PDF);
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/pipeline/loadMeta.ts
Normal file
108
src/pipeline/loadMeta.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { parse as parseYaml } from 'yaml';
|
||||||
|
|
||||||
|
import { DOCUMENTS_DIR } from '../config.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import { pathExists, readText } from '../utils/fs.js';
|
||||||
|
|
||||||
|
export interface DocumentMetaSection {
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
published?: string;
|
||||||
|
pubDate?: string;
|
||||||
|
copyright?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentMeta {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
published: string;
|
||||||
|
description?: string;
|
||||||
|
copyright: string;
|
||||||
|
frontCover?: DocumentMetaSection;
|
||||||
|
backCover?: DocumentMetaSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const META_FILES = ['_meta.yaml', '_meta.yml'];
|
||||||
|
|
||||||
|
const DEFAULT_META: DocumentMeta = {
|
||||||
|
title: 'Template',
|
||||||
|
author: 'ろむねこ',
|
||||||
|
published: '0000/000/00',
|
||||||
|
copyright: 'template',
|
||||||
|
description: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
function toRecord(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSection(value: unknown): DocumentMetaSection | undefined {
|
||||||
|
const record = toRecord(value);
|
||||||
|
if (!record) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const section: DocumentMetaSection = {};
|
||||||
|
const title = pickString(record.title);
|
||||||
|
const author = pickString(record.author);
|
||||||
|
const published = pickString(record.published);
|
||||||
|
const pubDate = pickString(record.pubDate);
|
||||||
|
const copyright = pickString(record.copyright);
|
||||||
|
|
||||||
|
if (title) section.title = title;
|
||||||
|
if (author) section.author = author;
|
||||||
|
if (published) section.published = published;
|
||||||
|
if (pubDate) section.pubDate = pubDate;
|
||||||
|
if (copyright) section.copyright = copyright;
|
||||||
|
|
||||||
|
return Object.keys(section).length > 0 ? section : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMeta(partial: Record<string, unknown>): DocumentMeta {
|
||||||
|
return {
|
||||||
|
title: pickString(partial.title) ?? DEFAULT_META.title,
|
||||||
|
author: pickString(partial.author) ?? DEFAULT_META.author,
|
||||||
|
published: pickString(partial.published) ?? DEFAULT_META.published,
|
||||||
|
description: pickString(partial.description) ?? DEFAULT_META.description,
|
||||||
|
copyright: pickString(partial.copyright) ?? DEFAULT_META.copyright,
|
||||||
|
frontCover: buildSection(partial.frontCover),
|
||||||
|
backCover: buildSection(partial.backCover)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadYamlMeta(filePath: string) {
|
||||||
|
const raw = await readText(filePath);
|
||||||
|
try {
|
||||||
|
const parsed = parseYaml(raw);
|
||||||
|
const record = toRecord(parsed);
|
||||||
|
if (!record) {
|
||||||
|
throw new Error('YAML の内容がオブジェクトではありません。');
|
||||||
|
}
|
||||||
|
return mergeMeta(record);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`YAML を解析できませんでした (${path.basename(filePath)}): ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMeta(): Promise<DocumentMeta> {
|
||||||
|
for (const filename of META_FILES) {
|
||||||
|
const fullPath = path.join(DOCUMENTS_DIR, filename);
|
||||||
|
if (await pathExists(fullPath)) {
|
||||||
|
logger.info(`メタ情報ファイル ${filename} を読み込みます`);
|
||||||
|
return loadYamlMeta(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info('メタ情報ファイルが見つからなかったため、テンプレートのデフォルト値を使用します');
|
||||||
|
return DEFAULT_META;
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import tidy from 'htmltidy2';
|
|||||||
|
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
import { writeText } from '../utils/fs.js';
|
import { writeText } from '../utils/fs.js';
|
||||||
|
import { DocumentMeta } from './loadMeta.js';
|
||||||
|
|
||||||
const renderFile = promisify(ejs.renderFile);
|
const renderFile = promisify(ejs.renderFile);
|
||||||
const tidyHtml = promisify(tidy.tidy);
|
const tidyHtml = promisify(tidy.tidy);
|
||||||
@ -13,9 +14,9 @@ const tidyHtml = promisify(tidy.tidy);
|
|||||||
* scripts/ejs.js (2SC1815J/md2pdf, MIT License) を TypeScript 化し、
|
* scripts/ejs.js (2SC1815J/md2pdf, MIT License) を TypeScript 化し、
|
||||||
* HTML Tidy 設定もフォーク元に合わせている。
|
* HTML Tidy 設定もフォーク元に合わせている。
|
||||||
*/
|
*/
|
||||||
export async function renderTemplate(templatePath: string, outputPath: string) {
|
export async function renderTemplate(templatePath: string, outputPath: string, meta: DocumentMeta) {
|
||||||
logger.info('テンプレート HTML を描画しています…');
|
logger.info('テンプレート HTML を描画しています…');
|
||||||
const rendered = await renderFile(templatePath);
|
const rendered = await renderFile(templatePath, { meta });
|
||||||
const tidied = await tidyHtml(rendered, {
|
const tidied = await tidyHtml(rendered, {
|
||||||
doctype: 'html5',
|
doctype: 'html5',
|
||||||
indent: 'auto',
|
indent: 'auto',
|
||||||
|
|||||||
@ -3,25 +3,28 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="openaction" content="#view=fit">
|
<meta name="openaction" content="#view=fit">
|
||||||
<meta name="author" content="r-ca" />
|
<meta name="author" content="<%= meta.author %>" />
|
||||||
<title>Template</title>
|
<% if (meta.description && meta.description.length > 0) { %>
|
||||||
|
<meta name="description" content="<%= meta.description %>" />
|
||||||
|
<% } %>
|
||||||
|
<title><%= meta.title %></title>
|
||||||
<link rel="stylesheet" href="../css/github-markdown.css">
|
<link rel="stylesheet" href="../css/github-markdown.css">
|
||||||
<link rel="stylesheet" href="../css/custom.css">
|
<link rel="stylesheet" href="../css/custom.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="FrontCover">
|
<div class="FrontCover">
|
||||||
<h1>Template</h1>
|
<h1><%= (meta.frontCover && meta.frontCover.title) || meta.title %></h1>
|
||||||
<div class="Published">0000/000/00</div>
|
<div class="Published"><%= (meta.frontCover && (meta.frontCover.published || meta.frontCover.pubDate)) || meta.published %></div>
|
||||||
<div class="Author">ろむねこ</div>
|
<div class="Author"><%= (meta.frontCover && meta.frontCover.author) || meta.author %></div>
|
||||||
</div>
|
</div>
|
||||||
<article class="markdown-body">
|
<article class="markdown-body">
|
||||||
<%- include('../work/all_md.html') %>
|
<%- include('../work/all_md.html') %>
|
||||||
</article>
|
</article>
|
||||||
<div class="BackCover">
|
<div class="BackCover">
|
||||||
<div class="Colophon">
|
<div class="Colophon">
|
||||||
<span class="Title">Template</span><br>
|
<span class="Title"><%= (meta.backCover && meta.backCover.title) || meta.title %></span><br>
|
||||||
<span class="PubDate">0000/000/00</span><br>
|
<span class="PubDate"><%= (meta.backCover && (meta.backCover.pubDate || meta.backCover.published)) || meta.published %></span><br>
|
||||||
<span class="Copyright">template</span>
|
<span class="Copyright"><%= (meta.backCover && meta.backCover.copyright) || meta.copyright %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user