features implemented

This commit is contained in:
Walid 2026-06-04 13:25:52 +06:00
parent ef185a00eb
commit fceb6f9f41
4 changed files with 237 additions and 101 deletions

146
main.js

File diff suppressed because one or more lines are too long

181
main.ts
View file

@ -51,6 +51,11 @@ interface PDFExportSettings extends DocStyle {
showFooter: boolean;
showPageNumbers: boolean;
pageNumberPosition: "center" | "left" | "right";
pageNumberStart: number;
showHeaderFooterOnFirstPage: boolean;
headerAlignment: "left" | "center" | "right";
hideFrontmatter: boolean;
customFontName: string;
autoBreakH1: boolean;
autoBreakH2: boolean;
includeFilenameAsTitle: boolean;
@ -80,7 +85,9 @@ interface PageLayout {
pageNodes: HTMLElement[];
pageNum: number;
totalPages: number;
headerText: string;
headerLeft: string;
headerCenter: string;
headerRight: string;
footerLeft: string;
footerRight: string;
footerCenter: string;
@ -256,6 +263,11 @@ const DEFAULT_SETTINGS: PDFExportSettings = {
showFooter: true,
showPageNumbers: true,
pageNumberPosition: "right",
pageNumberStart: 1,
showHeaderFooterOnFirstPage: true,
headerAlignment: "right",
hideFrontmatter: false,
customFontName: "",
autoBreakH1: false,
autoBreakH2: false,
includeFilenameAsTitle: false,
@ -285,6 +297,17 @@ function normalizeMarkdown(raw: string): string {
// which the plugin then styles distinctly via buildDocCSS.
}
/**
* Strips a YAML frontmatter block from the start of a markdown document.
* Matches the opening `---`, any content, and the closing `---` (with
* optional trailing whitespace on the fence lines), so it handles both
* Unix and Windows line endings. If no frontmatter is present the string
* is returned unchanged.
*/
function stripFrontmatter(md: string): string {
return md.replace(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
}
function splitMarkdownSections(md: string): string[] {
return md
.split(/^\/\/\/\s*$/m)
@ -399,6 +422,11 @@ function hexLuminance(hex: string): number {
function buildDocCSS(s: PDFExportSettings): string {
const hs = s.headingScale;
// Resolve custom font: when "__custom__" is selected use customFontName,
// otherwise use the preset's font stack directly.
const fontFamily = s.fontFamily === "__custom__"
? (s.customFontName.trim() || "inherit")
: s.fontFamily;
// Choose white or heading-colour text based on the actual background
// luminance, so customised colours work correctly regardless of preset name.
const tableHeaderTextColor =
@ -406,7 +434,7 @@ function buildDocCSS(s: PDFExportSettings): string {
return `
.mpdf-doc {
font-family: ${s.fontFamily};
font-family: ${fontFamily};
font-size: ${s.fontSize}px;
line-height: ${s.lineHeight};
color: ${s.bodyColor};
@ -526,7 +554,7 @@ function buildDocCSS(s: PDFExportSettings): string {
padding: 7px 12px !important;
background: ${s.accentColor}28 !important;
border-bottom: 1px solid ${s.accentColor}33 !important;
font-family: ${s.fontFamily} !important;
font-family: ${fontFamily} !important;
font-size: 0.8em !important;
font-weight: 800 !important;
font-style: normal !important;
@ -970,25 +998,50 @@ function paginateHTML(
function buildPageLayouts(allPages: HTMLElement[][], s: PDFExportSettings): PageLayout[] {
const totalPages = allPages.length;
return allPages.map((pageNodes, i) => {
const pageNum = i + 1;
const numStr = `${pageNum} / ${totalPages}`;
let footerLeft = "", footerRight = "", footerCenter = "";
const pageNum = i + 1;
// When first-page header/footer is disabled, page 0 is always blank.
const showHF = s.showHeaderFooterOnFirstPage || i > 0;
if (s.showPageNumbers) {
if (s.pageNumberPosition === "center") {
footerCenter = (s.footerText ? s.footerText + " — " : "") + numStr;
} else if (s.pageNumberPosition === "left") {
footerLeft = numStr;
footerRight = s.footerText ?? "";
// Compute the display number.
// enabled: page index 0 → pageNumberStart, 1 → pageNumberStart+1, …
// disabled: page index 1 → pageNumberStart, 2 → pageNumberStart+1, …
const displayNum = s.showHeaderFooterOnFirstPage
? s.pageNumberStart + i
: s.pageNumberStart + (i - 1);
const numStr = `${displayNum} / ${totalPages}`;
let footerLeft = "", footerRight = "", footerCenter = "";
let headerLeft = "", headerCenter = "", headerRight = "";
if (showHF) {
// ── Footer / page-number ───────────────────────────────────────────────
if (s.showPageNumbers) {
if (s.pageNumberPosition === "center") {
footerCenter = (s.footerText ? s.footerText + " — " : "") + numStr;
} else if (s.pageNumberPosition === "left") {
footerLeft = numStr;
footerRight = s.footerText ?? "";
} else {
footerLeft = s.footerText ?? "";
footerRight = numStr;
}
} else {
footerLeft = s.footerText ?? "";
footerRight = numStr;
footerLeft = s.footerText ?? "";
}
// ── Header ────────────────────────────────────────────────────────────
if (s.headerText) {
if (s.headerAlignment === "center") {
headerCenter = s.headerText;
} else if (s.headerAlignment === "left") {
headerLeft = s.headerText;
} else {
headerRight = s.headerText;
}
}
} else {
footerLeft = s.footerText ?? "";
}
return { pageNodes, pageNum, totalPages, headerText: s.headerText, footerLeft, footerRight, footerCenter };
return { pageNodes, pageNum, totalPages, headerLeft, headerCenter, headerRight, footerLeft, footerRight, footerCenter };
});
}
@ -1118,7 +1171,6 @@ class PDFExportModal extends Modal {
this.plugin.activeModal = this;
this.renderComponent.load();
this.modalEl.addClass("mpdf-modal");
this.contentEl.style.cssText = "display:flex;flex-direction:column;height:100%;padding:0;overflow:hidden;";
this.buildUI(this.contentEl);
const file = resolveActiveMarkdownFile(this.app, this.initialFile);
@ -1243,7 +1295,7 @@ class PDFExportModal extends Modal {
zoomSlider.max = "1.0";
zoomSlider.step = "0.05";
zoomSlider.value = String(s.previewScale);
zoomSlider.style.cssText = "width:64px;accent-color:var(--interactive-accent);";
zoomSlider.addClass("mpdf-zoom-slider");
zoomSlider.addEventListener("input", async () => {
const v = parseFloat(zoomSlider.value);
this.plugin.settings.previewScale = v;
@ -1401,6 +1453,12 @@ class PDFExportModal extends Modal {
const s = this.plugin.settings;
let md = normalizeMarkdown(this.editorEl.value);
// Strip YAML frontmatter when the option is enabled, so property blocks
// are hidden in both the preview and the exported PDF.
if (s.hideFrontmatter) {
md = stripFrontmatter(md);
}
// Prepend the file name as an H1 title when the option is enabled and a
// file is loaded. This mirrors Obsidian's built-in "Include file name as
// title" PDF export option, so the rendered document leads with the note
@ -1440,7 +1498,8 @@ class PDFExportModal extends Modal {
}
const layouts = buildPageLayouts(allPages, s);
this.layoutCache = { layouts, pw, ph, mTop, mLeft, footerH, headerH, contentW, contentH, docCSS, fontFamily: s.fontFamily, accentColor: s.accentColor, pageBackground: s.pageBackground };
const resolvedFont = s.fontFamily === "__custom__" ? (s.customFontName.trim() || "inherit") : s.fontFamily;
this.layoutCache = { layouts, pw, ph, mTop, mLeft, footerH, headerH, contentW, contentH, docCSS, fontFamily: resolvedFont, accentColor: s.accentColor, pageBackground: s.pageBackground };
this.drawPreview(this.layoutCache, s.previewScale);
this.pageCountEl.textContent = `${layouts.length} page${layouts.length !== 1 ? "s" : ""}`;
@ -1542,13 +1601,28 @@ class PDFExportModal extends Modal {
shadow.appendChild(styleEl);
// ── Header ───────────────────────────────────────────────────────────────
if (s.showHeader && layout.headerText) {
const hasHeader = s.showHeader && (layout.headerLeft || layout.headerCenter || layout.headerRight);
if (hasHeader) {
const hdr = document.createElement("div");
hdr.textContent = layout.headerText;
hdr.style.cssText = [
"position:absolute", `top:${mTop * 0.4}px`, `right:${mLeft}px`,
"position:absolute", `top:${mTop * 0.4}px`, `left:${mLeft}px`, `right:${mLeft}px`,
"display:flex", "align-items:center",
"font-size:9px", "color:#999", `font-family:${fontFamily}`, "white-space:nowrap",
].join(";");
if (layout.headerCenter) {
const span = document.createElement("span");
span.style.cssText = "flex:1;text-align:center;";
span.textContent = layout.headerCenter;
hdr.appendChild(span);
} else {
const leftSpan = document.createElement("span");
leftSpan.textContent = layout.headerLeft;
hdr.appendChild(leftSpan);
const rightSpan = document.createElement("span");
rightSpan.style.marginLeft = "auto";
rightSpan.textContent = layout.headerRight;
hdr.appendChild(rightSpan);
}
shadow.appendChild(hdr);
}
@ -1659,8 +1733,12 @@ class PDFExportModal extends Modal {
const pageHTMLParts = layouts.map((layout) => {
const contentHTML = layout.pageNodes.map((n) => n.outerHTML).join("\n");
const headerHTML = s.showHeader && layout.headerText
? `<div style="position:absolute;top:${mTop * 0.4}px;right:${mLeft}px;font-size:9px;color:#999;font-family:${fontFamily};white-space:nowrap;">${escapeHTML(layout.headerText)}</div>`
const hasExportHeader = s.showHeader && (layout.headerLeft || layout.headerCenter || layout.headerRight);
const headerInnerHTML = layout.headerCenter
? `<span style="flex:1;text-align:center;">${escapeHTML(layout.headerCenter)}</span>`
: `<span>${escapeHTML(layout.headerLeft)}</span><span style="margin-left:auto;">${escapeHTML(layout.headerRight)}</span>`;
const headerHTML = hasExportHeader
? `<div style="position:absolute;top:${mTop * 0.4}px;left:${mLeft}px;right:${mLeft}px;display:flex;align-items:center;font-size:9px;color:#999;font-family:${fontFamily};white-space:nowrap;">${headerInnerHTML}</div>`
: "";
const footerInnerHTML = layout.footerCenter
@ -1887,6 +1965,7 @@ class PDFExportSettingTab extends PluginSettingTab {
// ── Typography ────────────────────────────────────────────────────────────
containerEl.createEl("h3", { text: "Typography" });
let customFontSetting: Setting;
new Setting(containerEl).setName("Font family").addDropdown((d) =>
d.addOptions({
"Georgia, serif": "Georgia (Serif)",
@ -1896,9 +1975,23 @@ class PDFExportSettingTab extends PluginSettingTab {
"'Helvetica Neue', Helvetica, sans-serif": "Helvetica",
"'Trebuchet MS', sans-serif": "Trebuchet",
"'Courier New', monospace": "Courier New",
"__custom__": "Custom…",
}).setValue(s.fontFamily)
.onChange(async (v) => { s.fontFamily = v; await this.markDirty(); }),
.onChange(async (v) => {
s.fontFamily = v;
customFontSetting.settingEl.style.display = v === "__custom__" ? "" : "none";
await this.markDirty();
}),
);
customFontSetting = new Setting(containerEl)
.setName("Custom font name")
.setDesc("CSS font-family value — e.g. \"Inter, sans-serif\". The font must be installed on your system.")
.addText((t) =>
t.setPlaceholder("e.g. Inter, sans-serif")
.setValue(s.customFontName)
.onChange(async (v) => { s.customFontName = v; await this.markDirty(); }),
);
customFontSetting.settingEl.style.display = s.fontFamily === "__custom__" ? "" : "none";
new Setting(containerEl).setName("Font size (px)").addDropdown((d) => {
["10","11","12","13","14","15","16"].forEach((v) => d.addOption(v, v + "px"));
d.setValue(String(s.fontSize)).onChange(async (v) => {
@ -1973,8 +2066,13 @@ class PDFExportSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Header text")
.setDesc("Appears top-right on every page.")
.setDesc("Appears on every page according to the chosen alignment.")
.addText((t) => t.setValue(s.headerText).onChange(async (v) => { s.headerText = v; await this.markDirty(); }));
new Setting(containerEl).setName("Header alignment").addDropdown((d) =>
d.addOptions({ left: "Left", center: "Center", right: "Right" })
.setValue(s.headerAlignment)
.onChange(async (v) => { s.headerAlignment = v as "left"|"center"|"right"; await this.markDirty(); }),
);
new Setting(containerEl).setName("Show footer").addToggle((t) =>
t.setValue(s.showFooter).onChange(async (v) => { s.showFooter = v; await this.markDirty(); }),
);
@ -1989,9 +2087,38 @@ class PDFExportSettingTab extends PluginSettingTab {
.setValue(s.pageNumberPosition)
.onChange(async (v) => { s.pageNumberPosition = v as "left"|"center"|"right"; await this.markDirty(); }),
);
new Setting(containerEl)
.setName("Page number start")
.setDesc("Number assigned to the first visible page number. Accepts any integer.")
.addText((t) =>
t.setValue(String(s.pageNumberStart))
.onChange(async (v) => {
const n = parseInt(v, 10);
s.pageNumberStart = isNaN(n) ? 1 : n;
await this.markDirty();
}),
);
new Setting(containerEl)
.setName("Header/footer on first page")
.setDesc("When off, page 1 has no header, footer, or page number. Numbering begins on page 2 using the start value.")
.addToggle((t) =>
t.setValue(s.showHeaderFooterOnFirstPage).onChange(async (v) => {
s.showHeaderFooterOnFirstPage = v;
await this.markDirty();
}),
);
// ── Behaviour ─────────────────────────────────────────────────────────────
containerEl.createEl("h3", { text: "Behaviour" });
new Setting(containerEl)
.setName("Hide frontmatter")
.setDesc("Strip the YAML frontmatter block (--- … ---) from the preview and exported PDF.")
.addToggle((t) =>
t.setValue(s.hideFrontmatter).onChange(async (v) => {
s.hideFrontmatter = v;
await this.markDirty();
}),
);
new Setting(containerEl)
.setName("Include file name as title")
.setDesc(

View file

@ -2,7 +2,7 @@
"id": "advanced-pdf-export",
"name": "Advanced PDF Export",
"version": "3.1.0",
"minAppVersion": "0.15.0",
"minAppVersion": "1.11.1",
"description": "Export notes as pixel-perfect PDFs with live preview, style presets, manual page breaks, tables, and full layout control.",
"author": "ShrekBytes",
"authorUrl": "https://github.com/ShrekBytes",

View file

@ -14,6 +14,8 @@
padding: 0;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Top bar — three-section flex layout: controls | title | actions */
@ -271,3 +273,10 @@
Each preview page is now a Shadow DOM host; its background, box-shadow,
and dimensions are declared via :host {} inside the per-page shadow stylesheet
that drawPreview() generates from the active preset's values. */
/* Zoom slider in the topbar width and accent colour kept in CSS so the
inline style can be dropped from main.ts. */
.mpdf-zoom-slider {
width: 64px;
accent-color: var(--interactive-accent);
}