mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-12-23 08:47:50 +00:00
feat: add new pdf opts and image page number template to exporter (#2281)
Enhanced the exporter tool: - For PDF format, added `pdf.pdfVersion`, `pdf.pdfValidator` and `pdf.pdfTags`. `pdf.pdfVersion`, and `pdf.pdfValidator` will be combined into `pdfStandard`, and `pdf.pdfTags` will be inverted into `noPdfTags`. - Use `datetime-local` input for `pdf.pdfCreationTimestamp`. - For image formats, added `pageNumberTemplate` option, since we cannot customize that with output path for now. <img width="1028" height="391" alt="image" src="https://github.com/user-attachments/assets/1c0c44c9-0ea3-4f7c-a316-5a2746cb8a96" /> --------- Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
parent
ff76fb17d6
commit
c26dc8de85
6 changed files with 153 additions and 22 deletions
|
|
@ -1,7 +1,8 @@
|
|||
export interface ExportPdfOpts {
|
||||
pages?: string[];
|
||||
creationTimestamp?: string | null;
|
||||
// todo: pdf_standard
|
||||
pdfStandard?: string[];
|
||||
noPdfTags?: boolean;
|
||||
}
|
||||
|
||||
export interface PageMergeOpts {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ export interface ExportArgs {
|
|||
"svg.merged.gap"?: string;
|
||||
|
||||
"pdf.creationTimestamp"?: string | null;
|
||||
"pdf.pdfVersion"?: string;
|
||||
"pdf.pdfValidator"?: string;
|
||||
"pdf.pdfStandard"?: string[];
|
||||
"pdf.pdfTags"?: boolean;
|
||||
|
||||
"png.ppi"?: number;
|
||||
|
||||
|
|
@ -140,9 +144,21 @@ export const exportOps = (exportArgs: ExportArgs) => ({
|
|||
export const provideFormats = (exportArgs: ExportArgs, ops = exportOps(exportArgs)) => ({
|
||||
pdf: {
|
||||
opts(): ExportPdfOpts {
|
||||
const rawCreationTimestamp = exportArgs["pdf.creationTimestamp"];
|
||||
const creationTimestamp = rawCreationTimestamp?.includes("T")
|
||||
? Math.floor(new Date(rawCreationTimestamp).getTime() / 1000).toString() // datetime-local to unix timestamp
|
||||
: rawCreationTimestamp; // already unix timestamp or null/undefined
|
||||
|
||||
const pdfStandard = exportArgs["pdf.pdfStandard"] ?? [
|
||||
...(exportArgs["pdf.pdfVersion"] ? [exportArgs["pdf.pdfVersion"]] : []),
|
||||
...(exportArgs["pdf.pdfValidator"] ? [exportArgs["pdf.pdfValidator"]] : []),
|
||||
]; // combine version and validator into array
|
||||
|
||||
return {
|
||||
pages: ops.resolvePagesOpts("pdf"),
|
||||
creationTimestamp: exportArgs["pdf.creationTimestamp"],
|
||||
creationTimestamp,
|
||||
pdfStandard,
|
||||
noPdfTags: !exportArgs["pdf.pdfTags"], // invert to noPdfTags
|
||||
};
|
||||
},
|
||||
export: tinymist.exportPdf,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ const { div, h3, label, input, select, option, span, p } = van.tags;
|
|||
|
||||
interface OptionsPanelProps {
|
||||
format: ExportFormat;
|
||||
optionStates: Record<string, State<Scalar>>;
|
||||
// optionStates values can be scalar, an array (for multi-select), or undefined
|
||||
optionStates: Record<string, State<Scalar | Scalar[] | undefined>>;
|
||||
}
|
||||
|
||||
export const OptionsPanel = ({ format, optionStates }: OptionsPanelProps) => {
|
||||
|
|
@ -50,13 +51,17 @@ export const OptionsPanel = ({ format, optionStates }: OptionsPanelProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const OptionField = (schema: OptionSchema, valueState: State<Scalar>) => {
|
||||
const { key, label: optionLabel, description } = schema;
|
||||
const OptionField = (schema: OptionSchema, valueState: State<Scalar | Scalar[] | undefined>) => {
|
||||
const { key, label: optionLabel, description, type: optionType } = schema;
|
||||
const validationError = van.state<string | undefined>();
|
||||
const labelElem =
|
||||
optionType !== "boolean"
|
||||
? [label({ class: "text-sm font-medium", for: key }, optionLabel)]
|
||||
: [];
|
||||
|
||||
return div(
|
||||
{ class: "flex flex-col gap-xs" },
|
||||
label({ class: "text-sm font-medium", for: key }, optionLabel),
|
||||
...labelElem,
|
||||
renderInput(schema, valueState, validationError),
|
||||
() =>
|
||||
validationError.val
|
||||
|
|
@ -67,10 +72,10 @@ const OptionField = (schema: OptionSchema, valueState: State<Scalar>) => {
|
|||
|
||||
const renderInput = (
|
||||
schema: OptionSchema,
|
||||
valueState: State<Scalar | undefined>,
|
||||
valueState: State<Scalar | Scalar[] | undefined>,
|
||||
validationError: State<string | undefined>,
|
||||
) => {
|
||||
const { type, key, options: selectOptions, min, max } = schema;
|
||||
const { type, key, options: selectOptions, label: optionLabel } = schema;
|
||||
|
||||
switch (type) {
|
||||
case "string":
|
||||
|
|
@ -96,8 +101,8 @@ const renderInput = (
|
|||
const current = valueState.val;
|
||||
return current === undefined || current === null ? "" : String(current);
|
||||
},
|
||||
min: min?.toString() ?? null,
|
||||
max: max?.toString() ?? null,
|
||||
min: schema.min?.toString() ?? null,
|
||||
max: schema.max?.toString() ?? null,
|
||||
oninput: (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
// Call custom validation function if provided
|
||||
|
|
@ -119,7 +124,7 @@ const renderInput = (
|
|||
valueState.val = target.checked;
|
||||
},
|
||||
}),
|
||||
span({ class: "text-sm" }, "Enable"),
|
||||
label({ class: "text-sm font-medium", for: key }, optionLabel),
|
||||
);
|
||||
|
||||
case "color":
|
||||
|
|
@ -137,8 +142,57 @@ const renderInput = (
|
|||
},
|
||||
});
|
||||
|
||||
case "datetime":
|
||||
return input({
|
||||
class: "input",
|
||||
type: "datetime-local",
|
||||
id: key,
|
||||
value: () => {
|
||||
const current = valueState.val;
|
||||
return typeof current === "string" ? current : "";
|
||||
},
|
||||
onchange: (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
valueState.val = target.value;
|
||||
},
|
||||
});
|
||||
|
||||
case "select":
|
||||
if (!selectOptions) return span("No options available");
|
||||
// multi-select
|
||||
if (schema.multiple) {
|
||||
return select(
|
||||
{
|
||||
class: "select",
|
||||
id: key,
|
||||
multiple: true,
|
||||
onchange: (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const values = Array.from(target.selectedOptions).map((o) => o.value);
|
||||
const resolved = values
|
||||
.map((v) => selectOptions.find((opt) => opt.value.toString() === v))
|
||||
.filter((o): o is { value: Scalar; label: string } => Boolean(o))
|
||||
.map((opt) => opt.value);
|
||||
valueState.val = resolved;
|
||||
},
|
||||
},
|
||||
...selectOptions.map((opt) =>
|
||||
option(
|
||||
{
|
||||
value: opt.value.toString(),
|
||||
selected: () =>
|
||||
Array.isArray(valueState.val) &&
|
||||
(valueState.val as Scalar[])
|
||||
.map((v) => v?.toString())
|
||||
.includes(opt.value.toString()),
|
||||
},
|
||||
opt.label,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// single-select
|
||||
return select(
|
||||
{
|
||||
class: "select",
|
||||
|
|
@ -156,6 +210,7 @@ const renderInput = (
|
|||
valueState.val = newValue;
|
||||
},
|
||||
},
|
||||
schema.default ? null : option({ value: "" }, "None"), // if no default, add a "None" option
|
||||
...selectOptions.map((opt) =>
|
||||
option(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export function useExporter() {
|
|||
const inputPath = van.state("");
|
||||
const outputPath = van.state("");
|
||||
const format = van.state(EXPORT_FORMATS[0]);
|
||||
const optionStates: Record<string, State<Scalar>> = {};
|
||||
// option value may be scalar or an array (multi-select)
|
||||
const optionStates: Record<string, State<Scalar | Scalar[] | undefined>> = {};
|
||||
|
||||
let previewVersion = 0;
|
||||
const previewGenerating = van.state(false);
|
||||
|
|
@ -20,7 +21,10 @@ export function useExporter() {
|
|||
const extraOpts = Object.fromEntries(
|
||||
format.val.options.map((option) => {
|
||||
const val = optionStates[option.key]?.val;
|
||||
return [option.key, val === "" ? undefined : val];
|
||||
// treat empty strings or empty arrays as undefined so the server can pick defaults
|
||||
if (val === "") return [option.key, undefined];
|
||||
if (Array.isArray(val) && val.length === 0) return [option.key, undefined];
|
||||
return [option.key, val];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ const PAGES_OPT: OptionSchema = {
|
|||
validate: validatePageRanges,
|
||||
};
|
||||
|
||||
const IMAGE_PAGES_OPTS: OptionSchema[] = [
|
||||
PAGES_OPT,
|
||||
{
|
||||
key: "pageNumberTemplate",
|
||||
type: "string",
|
||||
label: "Page Number Template",
|
||||
description:
|
||||
'Template used to render page numbers when exporting multiple pages (e.g., "Page {n}")',
|
||||
default: "",
|
||||
},
|
||||
];
|
||||
|
||||
const MERGE_OPTS: OptionSchema[] = [
|
||||
{
|
||||
key: "merged",
|
||||
|
|
@ -36,10 +48,9 @@ export const EXPORT_FORMATS: ExportFormat[] = [
|
|||
PAGES_OPT,
|
||||
{
|
||||
key: "pdf.creationTimestamp",
|
||||
type: "string",
|
||||
type: "datetime",
|
||||
label: "Creation Timestamp",
|
||||
description:
|
||||
"The document's creation date formatted as a UNIX timestamp. (leave empty for current time)",
|
||||
description: "The document's creation date (leave empty for current time)",
|
||||
default: "",
|
||||
validate: (value: string) => {
|
||||
if (value.trim() === "") {
|
||||
|
|
@ -54,6 +65,46 @@ export const EXPORT_FORMATS: ExportFormat[] = [
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "pdf.pdfVersion",
|
||||
type: "select",
|
||||
label: "PDF Version",
|
||||
description: "Optional version of the PDF document",
|
||||
options: [
|
||||
{ value: "1.4", label: "PDF 1.4" },
|
||||
{ value: "1.5", label: "PDF 1.5" },
|
||||
{ value: "1.6", label: "PDF 1.6" },
|
||||
{ value: "1.7", label: "PDF 1.7" },
|
||||
{ value: "2.0", label: "PDF 2.0" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "pdf.pdfValidator",
|
||||
type: "select",
|
||||
label: "PDF Validator",
|
||||
description: "Optional validator for exporting PDF documents to a specific subset of PDF",
|
||||
options: [
|
||||
{ value: "a-1b", label: "PDF/A-1b" },
|
||||
{ value: "a-1a", label: "PDF/A-1a" },
|
||||
{ value: "a-2b", label: "PDF/A-2b" },
|
||||
{ value: "a-2u", label: "PDF/A-2u" },
|
||||
{ value: "a-2a", label: "PDF/A-2a" },
|
||||
{ value: "a-3b", label: "PDF/A-3b" },
|
||||
{ value: "a-3u", label: "PDF/A-3u" },
|
||||
{ value: "a-3a", label: "PDF/A-3a" },
|
||||
{ value: "a-4", label: "PDF/A-4" },
|
||||
{ value: "a-4f", label: "PDF/A-4f" },
|
||||
{ value: "a-4e", label: "PDF/A-4e" },
|
||||
{ value: "ua-1", label: "PDF/UA-1" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "pdf.pdfTags",
|
||||
type: "boolean",
|
||||
label: "PDF Tags",
|
||||
description: "Include tagged structure in the PDF for better accessibility.",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -61,7 +112,7 @@ export const EXPORT_FORMATS: ExportFormat[] = [
|
|||
label: "PNG",
|
||||
fileExtension: "png",
|
||||
options: [
|
||||
PAGES_OPT,
|
||||
...IMAGE_PAGES_OPTS,
|
||||
...MERGE_OPTS,
|
||||
{
|
||||
key: "png.ppi",
|
||||
|
|
@ -90,7 +141,7 @@ export const EXPORT_FORMATS: ExportFormat[] = [
|
|||
id: "svg",
|
||||
label: "SVG",
|
||||
fileExtension: "svg",
|
||||
options: [PAGES_OPT, ...MERGE_OPTS],
|
||||
options: [...IMAGE_PAGES_OPTS, ...MERGE_OPTS],
|
||||
},
|
||||
{
|
||||
id: "html",
|
||||
|
|
|
|||
|
|
@ -4,16 +4,20 @@ export type ExportFormatId = "pdf" | "png" | "svg" | "html" | "markdown" | "tex"
|
|||
|
||||
export interface OptionSchema {
|
||||
key: string;
|
||||
type: "string" | "number" | "boolean" | "color" | "select";
|
||||
type: "string" | "number" | "boolean" | "color" | "datetime" | "select";
|
||||
label: string;
|
||||
description?: string;
|
||||
default: Scalar;
|
||||
// default can be a single scalar or an array of scalars for multi-select
|
||||
default?: Scalar | Scalar[];
|
||||
options?: Array<{ value: Scalar; label: string }>;
|
||||
// when true and type === 'select', UI should render a multi-select
|
||||
multiple?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
dependsOn?: string; // Key of another option that this option depends on
|
||||
// Custom validation function for string fields
|
||||
validate?: (value: string) => string | undefined; // Returns error message or undefined if valid
|
||||
// Custom validation function for string fields.
|
||||
// Returns a string error message when invalid, or undefined when valid.
|
||||
validate?: (value: string) => string | undefined;
|
||||
}
|
||||
|
||||
export interface ExportFormat {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue