feat: fold and punch marks (Falzmarken, DIN 5008)
edentext-fold-marks flag: FoldMarkLayer draws the 87/192mm folds and the 148.5mm punch mark per page inside .paper, so print and PDF include them. Toggled in the Layout tab. Exported as named EdenFoldMark* lines in the master page's header (ODF page-relative draw:line, DOCX VML v:line); both importers read the names back into the flag. Roundtrip Leg 32.
This commit is contained in:
1 parent
0753caa5a9
commit
a5c072db23
16 files changed
+265
-16
No files matched your search
+12
-5
@@ -44,6 +44,7 @@
|
||||
import { loadPageNumbering, savePageNumbering, DEFAULT_PAGE_NUMBERING, type PageNumbering } from './lib/storage/pageNumbering';
|
||||
import { loadPageDecor, savePageDecor, EMPTY_PAGE_DECOR, type PageDecor } from './lib/storage/pageDecor';
|
||||
import { loadLineNumbering, saveLineNumbering, DEFAULT_LINE_NUMBERING, type LineNumbering } from './lib/storage/lineNumbering';
|
||||
import { loadFoldMarks, saveFoldMarks } from './lib/storage/foldMarks';
|
||||
import { loadDocumentLanguage, saveDocumentLanguage, odfFromLanguage, type DocumentLanguage } from './lib/storage/documentLanguage';
|
||||
import { setTableLanguage } from './lib/storage/tableOptions.svelte';
|
||||
import { spellController } from './lib/spell/controller';
|
||||
@@ -182,6 +183,7 @@
|
||||
let pageNumbering: PageNumbering = $state(loadPageNumbering());
|
||||
let pageDecor: PageDecor = $state(loadPageDecor());
|
||||
let lineNumbering: LineNumbering = $state(loadLineNumbering());
|
||||
let foldMarks = $state(loadFoldMarks());
|
||||
let autoCorrectOpen = $state(false);
|
||||
let autoTextOpen = $state(false);
|
||||
let thesaurusOpen = $state(false);
|
||||
@@ -266,6 +268,7 @@
|
||||
savePageNumbering(pageNumbering);
|
||||
savePageDecor(pageDecor);
|
||||
saveLineNumbering(lineNumbering);
|
||||
saveFoldMarks(foldMarks);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -560,6 +563,7 @@
|
||||
pageNumbering = { ...DEFAULT_PAGE_NUMBERING };
|
||||
pageDecor = { ...EMPTY_PAGE_DECOR };
|
||||
lineNumbering = { ...DEFAULT_LINE_NUMBERING };
|
||||
foldMarks = false;
|
||||
// Recording belongs to the document, so a new one starts off, as it does in both.
|
||||
setRecordChanges(false);
|
||||
docProps = { ...EMPTY_DOC_PROPERTIES };
|
||||
@@ -639,6 +643,7 @@
|
||||
pageNumbering = result.pageNumbering;
|
||||
pageDecor = result.decor;
|
||||
lineNumbering = result.lineNumbering;
|
||||
foldMarks = result.foldMarks === true;
|
||||
// The file says whether it goes on recording; ours is not the setting that counts.
|
||||
setRecordChanges(result.recordChanges);
|
||||
// Adopt the document's spell-check language (the $effect switches the
|
||||
@@ -773,12 +778,12 @@
|
||||
// reference word processors — not silently rewritten to .odt under its old name.
|
||||
if (documentFormat === 'docx') {
|
||||
const { buildDocx } = await import('./lib/export/docx');
|
||||
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges());
|
||||
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges(), foldMarks);
|
||||
fileHandle = await saveDocx(bytes, suggestedFilenameDocx(json), fileHandle);
|
||||
recentFiles = await rememberRecentFile(fileHandle?.name ?? suggestedFilenameDocx(json), fileHandle);
|
||||
return;
|
||||
}
|
||||
const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges());
|
||||
const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges(), foldMarks);
|
||||
fileHandle = await saveOdt(bytes, suggestedFilename(json), fileHandle);
|
||||
recentFiles = await rememberRecentFile(fileHandle?.name ?? suggestedFilename(json), fileHandle);
|
||||
} catch (err) {
|
||||
@@ -795,7 +800,7 @@
|
||||
exportMenuOpen = false;
|
||||
const json = editor.getJSON() as Parameters<typeof buildOdt>[0];
|
||||
try {
|
||||
const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges());
|
||||
const bytes = await buildOdt(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges(), foldMarks);
|
||||
fileHandle = await saveAsOdt(bytes, suggestedFilename(json));
|
||||
documentFormat = 'odt'; // Save As is odt-only, so a docx-opened document switches format here.
|
||||
recentFiles = await rememberRecentFile(fileHandle?.name ?? suggestedFilename(json), fileHandle);
|
||||
@@ -815,7 +820,7 @@
|
||||
const json = editor.getJSON() as Parameters<typeof buildOdt>[0];
|
||||
try {
|
||||
await saveAsTemplate(async (kind) => {
|
||||
const args = [pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges()] as const;
|
||||
const args = [pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges(), foldMarks] as const;
|
||||
const { odtToOtt, docxToDotx } = await import('./lib/export/template');
|
||||
if (kind === 'dotx') {
|
||||
const { buildDocx } = await import('./lib/export/docx');
|
||||
@@ -868,7 +873,7 @@
|
||||
try {
|
||||
const json = editor.getJSON() as Parameters<typeof buildOdt>[0];
|
||||
const { buildDocx } = await import('./lib/export/docx');
|
||||
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges());
|
||||
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings(), docProps, hyphenate, pageNumbering, pageDecor, lineNumbering, recordChanges(), foldMarks);
|
||||
await saveAsDocx(bytes, suggestedFilenameDocx(json));
|
||||
} catch (err) {
|
||||
if ((err as DOMException)?.name === 'AbortError') return;
|
||||
@@ -1100,6 +1105,7 @@
|
||||
bind:pageNumbering
|
||||
bind:pageDecor
|
||||
bind:lineNumbering
|
||||
bind:foldMarks
|
||||
{hfActive}
|
||||
onManageStyles={openStyleManager}
|
||||
onManageTableStyles={() => openStyleManager('table')}
|
||||
@@ -1434,6 +1440,7 @@
|
||||
{pageNumbering}
|
||||
{pageDecor}
|
||||
{lineNumbering}
|
||||
{foldMarks}
|
||||
bind:extraHfSections
|
||||
{zoom}
|
||||
onZoom={setZoom}
|
||||
|
||||
@@ -199,6 +199,12 @@ per page in unscaled document px. The watermark is an `<svg><text textLength>` b
|
||||
fontwork shape **stretches** its text to its box instead of setting it at a size — a font
|
||||
size alone makes a short word far too small and a long one overflow the page.
|
||||
|
||||
## Fold marks (`FoldMarkLayer.svelte`, `storage/foldMarks.ts`)
|
||||
|
||||
Fold + punch marks at the DIN 5008 positions, drawn per page in the left margin from
|
||||
`pageBoxes`. Inside the scaled `.paper`, so print and the PDF raster include them; the
|
||||
Layout tab's toggle flips the flag.
|
||||
|
||||
## Line numbering (`LineNumberLayer.svelte`, `storage/lineNumbering.ts`)
|
||||
|
||||
One number per rendered line in the left margin. There is no CSS line box to read, so
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import PageDecorLayer from './PageDecorLayer.svelte';
|
||||
import PageSheetLayer from './PageSheetLayer.svelte';
|
||||
import LineNumberLayer from './LineNumberLayer.svelte';
|
||||
import FoldMarkLayer from './FoldMarkLayer.svelte';
|
||||
import { DEFAULT_LINE_NUMBERING, type LineNumbering } from '../storage/lineNumbering';
|
||||
import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
import Ruler from './Ruler.svelte';
|
||||
@@ -54,7 +55,7 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
let {
|
||||
editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1),
|
||||
zoom = 100, onZoom, showFormattingMarks = false, showRuler = true, splitView = false, pageColumns = 1, pageMargins = DEFAULT_MARGINS, orientation = 'portrait',
|
||||
pageFormat = 'A4', tabIntervalCm = DEFAULT_TAB_INTERVAL_CM, spacingModel = 'add', documentEpoch = 0, pageRtl = false, hyphenate = false, documentLanguage = 'en', pageNumbering = DEFAULT_PAGE_NUMBERING, pageDecor = EMPTY_PAGE_DECOR, lineNumbering = DEFAULT_LINE_NUMBERING,
|
||||
pageFormat = 'A4', tabIntervalCm = DEFAULT_TAB_INTERVAL_CM, spacingModel = 'add', documentEpoch = 0, pageRtl = false, hyphenate = false, documentLanguage = 'en', pageNumbering = DEFAULT_PAGE_NUMBERING, pageDecor = EMPTY_PAGE_DECOR, lineNumbering = DEFAULT_LINE_NUMBERING, foldMarks = false,
|
||||
headerDoc = $bindable(null), footerDoc = $bindable(null), hfDistances = DEFAULT_HF_DISTANCES,
|
||||
headerFirstDoc = $bindable(null), footerFirstDoc = $bindable(null), differentFirstPage = false,
|
||||
headerEvenDoc = $bindable(null), footerEvenDoc = $bindable(null), differentOddEven = false,
|
||||
@@ -80,6 +81,8 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
pageDecor?: PageDecor;
|
||||
/** Numbers in the left margin, one per rendered line. */
|
||||
lineNumbering?: LineNumbering;
|
||||
/** Fold + punch marks in the left margin (DIN 5008 letter sheets). */
|
||||
foldMarks?: boolean;
|
||||
headerDoc?: HfDoc; footerDoc?: HfDoc; hfDistances?: HfDistances;
|
||||
headerFirstDoc?: HfDoc; footerFirstDoc?: HfDoc; differentFirstPage?: boolean;
|
||||
headerEvenDoc?: HfDoc; footerEvenDoc?: HfDoc; differentOddEven?: boolean;
|
||||
@@ -1450,6 +1453,9 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor';
|
||||
{/if}
|
||||
<PageSheetLayer {pageBoxes} />
|
||||
<PageDecorLayer decor={pageDecor} {pageBoxes} {pageMargins} />
|
||||
{#if foldMarks}
|
||||
<FoldMarkLayer {pageBoxes} />
|
||||
{/if}
|
||||
{#if lineNumbering.on}
|
||||
<LineNumberLayer {editor} {tick} {lineNumbering} {pageBoxes} {pageMargins} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { PX_PER_CM } from '../storage/pageMargins';
|
||||
import { FOLD_MARK_MM, PUNCH_MARK_MM, MARK_START_MM, FOLD_MARK_LEN_MM, PUNCH_MARK_LEN_MM } from '../storage/foldMarks';
|
||||
|
||||
// Fold + punch marks in the left margin of every page, positioned from the page's
|
||||
// own corner in unscaled document px (the zoom transform on .paper covers it).
|
||||
let { pageBoxes }: {
|
||||
pageBoxes: { top: number; left: number; height: number; width: number }[];
|
||||
} = $props();
|
||||
|
||||
const mmToPx = (mm: number) => (mm / 10) * PX_PER_CM;
|
||||
const marks = [
|
||||
...FOLD_MARK_MM.map((mm) => ({ mm, len: FOLD_MARK_LEN_MM })),
|
||||
{ mm: PUNCH_MARK_MM, len: PUNCH_MARK_LEN_MM },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="fold-mark-layer" aria-hidden="true">
|
||||
{#each pageBoxes as box}
|
||||
{#each marks as m}
|
||||
<span
|
||||
class="fold-mark"
|
||||
style="top: {box.top + mmToPx(m.mm)}px; left: {box.left + mmToPx(MARK_START_MM)}px; width: {mmToPx(m.len)}px;"
|
||||
></span>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* A sibling of the other layers inside the scaled .paper; the marks are the sheet's
|
||||
own ink, so they keep the fixed page-text color through every theme. */
|
||||
.fold-mark-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fold-mark {
|
||||
position: absolute;
|
||||
border-top: 1px solid var(--color-page-text);
|
||||
}
|
||||
</style>
|
||||
@@ -56,6 +56,7 @@
|
||||
pageNumbering = $bindable(DEFAULT_PAGE_NUMBERING),
|
||||
pageDecor = $bindable(EMPTY_PAGE_DECOR),
|
||||
lineNumbering = $bindable(DEFAULT_LINE_NUMBERING),
|
||||
foldMarks = $bindable(false),
|
||||
hfActive = null,
|
||||
onManageStyles,
|
||||
onManageTableStyles,
|
||||
@@ -94,6 +95,7 @@
|
||||
pageNumbering?: PageNumbering;
|
||||
pageDecor?: PageDecor;
|
||||
lineNumbering?: LineNumbering;
|
||||
foldMarks?: boolean;
|
||||
hfActive?: HfZone | null;
|
||||
onManageStyles?: (family: StyleFamily) => void;
|
||||
onManageTableStyles?: (family: StyleFamily) => void;
|
||||
@@ -350,7 +352,7 @@
|
||||
{:else if tab === 'insert'}
|
||||
<InsertTab {editor} {tick} {hfActive} {pageMargins} {pageOrientation} {pageFormat} {onEditZone} {onManageTableStyles} {onAutoText} />
|
||||
{:else if tab === 'layout'}
|
||||
<LayoutTab {editor} {tick} {hfActive} bind:pageMargins bind:pageOrientation bind:pageFormat bind:extraHfSections bind:hyphenate bind:pageNumbering bind:pageDecor bind:lineNumbering onParagraphDialog={() => (paragraphDialogOpen = true)} />
|
||||
<LayoutTab {editor} {tick} {hfActive} bind:pageMargins bind:pageOrientation bind:pageFormat bind:extraHfSections bind:hyphenate bind:pageNumbering bind:pageDecor bind:lineNumbering bind:foldMarks onParagraphDialog={() => (paragraphDialogOpen = true)} />
|
||||
{:else if tab === 'references'}
|
||||
<ReferencesTab {editor} {tick} {hfActive} {onNoteOptions} />
|
||||
{:else if tab === 'review'}
|
||||
|
||||
@@ -89,6 +89,7 @@ export const STROKED = {
|
||||
trackChanges: ['M2 4h12M2 8h7', 'M9.5 13.5 14 9l-1.6-1.6L7.9 12z', 'M7.9 12 7.4 14.1l2.1-.6'],
|
||||
close: ['M3.5 3.5 12.5 12.5M12.5 3.5 3.5 12.5'],
|
||||
lineNumbers: ['M6 3h8.5M6 6.5h8.5M6 10h8.5M6 13.5h8.5', 'M2 2.2h1.1v3.3M1.6 5.5h1.9', 'M1.5 9.6c.2-.4.6-.5 1-.4.5.1.6.6.3 1L1.6 11.6h1.9'],
|
||||
foldMarks: ['M4.5 2h9v12h-9z', 'M1.5 5h2M1.5 8.2h2.8M1.5 11.4h2'],
|
||||
wordCount: ['M2.5 2.5h11v11h-11z', 'M5 6h6M5 9h4'],
|
||||
// An open book, the glyph both word processors give the thesaurus.
|
||||
thesaurus: ['M8 4.5C6.5 3.4 4.4 3.1 2.2 3.3v9c2.2-.2 4.3.1 5.8 1.2 1.5-1.1 3.6-1.4 5.8-1.2v-9c-2.2-.2-4.3.1-5.8 1.2z', 'M8 4.5v9'],
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
pageNumbering = $bindable(DEFAULT_PAGE_NUMBERING),
|
||||
pageDecor = $bindable(EMPTY_PAGE_DECOR),
|
||||
lineNumbering = $bindable(DEFAULT_LINE_NUMBERING),
|
||||
foldMarks = $bindable(false),
|
||||
onParagraphDialog,
|
||||
}: {
|
||||
editor: Editor | null;
|
||||
@@ -40,6 +41,7 @@
|
||||
pageNumbering?: PageNumbering;
|
||||
pageDecor?: PageDecor;
|
||||
lineNumbering?: LineNumbering;
|
||||
foldMarks?: boolean;
|
||||
onParagraphDialog?: () => void;
|
||||
} = $props();
|
||||
|
||||
@@ -365,6 +367,14 @@
|
||||
active={!!(pageDecor.background || pageDecor.border || pageDecor.watermark)}
|
||||
onclick={() => (decorOpen = true)}
|
||||
/>
|
||||
<RibbonButton
|
||||
variant="small"
|
||||
icon="foldMarks"
|
||||
label={t().ribbon.foldMarks}
|
||||
title={t().ribbon.foldMarksHint}
|
||||
active={foldMarks}
|
||||
onclick={() => (foldMarks = !foldMarks)}
|
||||
/>
|
||||
</div>
|
||||
</RibbonGroup>
|
||||
|
||||
|
||||
+40
-6
@@ -40,6 +40,7 @@ import { normalizeColor, GENERATOR, MAX_HEADING_LEVEL, mergeJoinedParagraphsJson
|
||||
import { EMPTY_DOC_PROPERTIES, type DocProperties } from '../storage/docProperties';
|
||||
import { DEFAULT_PAGE_NUMBERING, type PageNumbering } from '../storage/pageNumbering';
|
||||
import { EMPTY_PAGE_DECOR, isEmptyPageDecor, type PageDecor, type Watermark } from '../storage/pageDecor';
|
||||
import { FOLD_MARK_MM, PUNCH_MARK_MM, MARK_START_MM, FOLD_MARK_LEN_MM, PUNCH_MARK_LEN_MM, FOLD_MARK_NAME } from '../storage/foldMarks';
|
||||
import { DEFAULT_LINE_NUMBERING, type LineNumbering } from '../storage/lineNumbering';
|
||||
|
||||
// The five page-number formats both word processors offer → Word's own names.
|
||||
@@ -1515,6 +1516,37 @@ function applyPageDecorDocx(bytes: Uint8Array, decor: PageDecor, widthPt: number
|
||||
return zipSync(out);
|
||||
}
|
||||
|
||||
// Post-pack pass: fold + punch marks as named VML lines in every header part — the one
|
||||
// place that repeats on every page — positioned from the page corner like the watermark.
|
||||
function applyFoldMarksDocx(bytes: Uint8Array, on: boolean): Uint8Array {
|
||||
if (!on) return bytes;
|
||||
const files = unzipSync(bytes);
|
||||
const pt = (mm: number) => Math.round((mm / 25.4) * 72 * 100) / 100;
|
||||
const line = (id: string, mm: number, lenMm: number) =>
|
||||
`<v:line id="${id}" o:allowincell="f"` +
|
||||
` style="position:absolute;mso-position-horizontal-relative:page;mso-position-vertical-relative:page;mso-wrap-style:none"` +
|
||||
` from="${pt(MARK_START_MM)}pt,${pt(mm)}pt" to="${pt(MARK_START_MM + lenMm)}pt,${pt(mm)}pt"` +
|
||||
` strokecolor="black" strokeweight=".72pt"><w10:wrap type="none"/></v:line>`;
|
||||
const shapes = '<w:pict>'
|
||||
+ FOLD_MARK_MM.map((mm, i) => line(`${FOLD_MARK_NAME}${i + 1}`, mm, FOLD_MARK_LEN_MM)).join('')
|
||||
+ line(`${FOLD_MARK_NAME}Punch`, PUNCH_MARK_MM, PUNCH_MARK_LEN_MM) + '</w:pict>';
|
||||
let changed = false;
|
||||
for (const path of Object.keys(files)) {
|
||||
if (!/^word\/header\d*\.xml$/.test(path)) continue;
|
||||
const xml = strFromU8(files[path]);
|
||||
if (xml.includes(FOLD_MARK_NAME)) continue;
|
||||
// Inside a paragraph, like the watermark: as a child of w:hdr the pict is dropped.
|
||||
files[path] = strToU8(xml.replace(/<w:p\b[^>]*\/>|(<w:p\b[^>]*>(?:<w:pPr>.*?<\/w:pPr>)?)/,
|
||||
(m, opening: string | undefined) =>
|
||||
opening ? `${opening}<w:r>${shapes}</w:r>` : `<w:p><w:r>${shapes}</w:r></w:p>`));
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return bytes;
|
||||
const out: Record<string, [Uint8Array, { level: 6 }]> = {};
|
||||
for (const [path, data] of Object.entries(files)) out[path] = [data, { level: 6 }];
|
||||
return zipSync(out);
|
||||
}
|
||||
|
||||
const WATERMARK_NAME = 'PowerPlusWaterMarkObject';
|
||||
// The shape's aspect ratio, measured off LibreOffice's own watermark.
|
||||
const WATERMARK_RATIO = 4.487;
|
||||
@@ -2212,6 +2244,7 @@ export async function buildDocx(
|
||||
decor: PageDecor = EMPTY_PAGE_DECOR,
|
||||
lineNumbering: LineNumbering = DEFAULT_LINE_NUMBERING,
|
||||
recordChanges = false,
|
||||
foldMarks = false,
|
||||
): Promise<Uint8Array> {
|
||||
docLangTag = localeTag(language ? language.language : 'en');
|
||||
exportSheet = styles;
|
||||
@@ -2303,12 +2336,12 @@ export async function buildDocx(
|
||||
const mkHeaders = (i: number) => {
|
||||
const s = setAt(i);
|
||||
const d = para(s.header), f = s.differentFirstPage ? para(s.headerFirst) : null, e = s.differentOddEven ? para(s.headerEven) : null;
|
||||
// A watermark lives in a header part, so a document without one still needs the
|
||||
// (empty) default header for applyPageDecorDocx to inject the shape into.
|
||||
if (!d && !f && !e) return decor.watermark?.text ? { default: new Header({ children: [new Paragraph({})] }) } : undefined;
|
||||
// A watermark (and the fold marks) lives in a header part, so a document without
|
||||
// one still needs the (empty) default header the post-passes inject into.
|
||||
if (!d && !f && !e) return decor.watermark?.text || foldMarks ? { default: new Header({ children: [new Paragraph({})] }) } : undefined;
|
||||
const h: { default?: Header; first?: Header; even?: Header } = {};
|
||||
if (d) h.default = new Header({ children: [paragraphToDocx(d)] });
|
||||
else if (decor.watermark?.text) h.default = new Header({ children: [new Paragraph({})] });
|
||||
else if (decor.watermark?.text || foldMarks) h.default = new Header({ children: [new Paragraph({})] });
|
||||
if (f) h.first = new Header({ children: [paragraphToDocx(f)] });
|
||||
if (e) h.even = new Header({ children: [paragraphToDocx(e)] });
|
||||
return h;
|
||||
@@ -2387,9 +2420,10 @@ export async function buildDocx(
|
||||
const withResolved = applyCommentsResolvedDocx(withNotes);
|
||||
const mirrored = margins.mirrored ? applyMirrorMarginsDocx(withResolved) : withResolved;
|
||||
const bidi = applyNoHyphensDocx(rtl ? applyBidiDocx(mirrored) : mirrored);
|
||||
if (isEmptyPageDecor(decor)) return bidi;
|
||||
const marked = applyFoldMarksDocx(bidi, foldMarks);
|
||||
if (isEmptyPageDecor(decor)) return marked;
|
||||
const dims = pageDimsCm(pageFormat, orientation);
|
||||
const pt = (cm: number) => (cm / 2.54) * 72;
|
||||
return applyPageDecorDocx(bidi, decor,
|
||||
return applyPageDecorDocx(marked, decor,
|
||||
pt(dims.w - margins.left - margins.right), pt(dims.h - margins.top - margins.bottom));
|
||||
}
|
||||
+46
-2
@@ -10,6 +10,7 @@ import { DEFAULT_NOTE_SETTINGS, NOTE_FONT_SIZE_PT, NOTE_INDENT_CM, type NoteKind
|
||||
import { EMPTY_DOC_PROPERTIES, keywordList, type DocProperties } from '../storage/docProperties';
|
||||
import { DEFAULT_PAGE_NUMBERING, type PageNumbering } from '../storage/pageNumbering';
|
||||
import { EMPTY_PAGE_DECOR, type PageDecor, type Watermark } from '../storage/pageDecor';
|
||||
import { FOLD_MARK_MM, PUNCH_MARK_MM, MARK_START_MM, FOLD_MARK_LEN_MM, PUNCH_MARK_LEN_MM, FOLD_MARK_NAME } from '../storage/foldMarks';
|
||||
import { DEFAULT_LINE_NUMBERING, type LineNumbering } from '../storage/lineNumbering';
|
||||
import { builtinStyleSheet, DEFAULT_STYLE, resolveStyle, type StyleSheet, type TextProps } from '../styles/styleSheet';
|
||||
import {
|
||||
@@ -2773,6 +2774,49 @@ function applyWatermarkOdf(odtBytes: Uint8Array, wm: Watermark | null): Uint8Arr
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// Fold + punch marks as named <draw:line>s in the master page's header — the one place
|
||||
// that repeats on every page, where LibreOffice's own DIN letter templates keep them.
|
||||
// Page-relative position (from-top/from-left against "page"), out of flow like the
|
||||
// watermark; the importers read the EdenFoldMark names back into the flag.
|
||||
function applyFoldMarksOdf(odtBytes: Uint8Array, on: boolean): Uint8Array {
|
||||
if (!on) return odtBytes;
|
||||
const files = unzipSync(odtBytes);
|
||||
const stylesBytes = files['styles.xml'];
|
||||
if (!stylesBytes) return odtBytes;
|
||||
const styles = strFromU8(stylesBytes);
|
||||
const minted =
|
||||
`<style:style style:name="FoldMark" style:family="graphic"><style:graphic-properties` +
|
||||
` draw:stroke="solid" svg:stroke-color="#000000" svg:stroke-width="0.026cm" draw:fill="none"` +
|
||||
` style:run-through="background" style:wrap="run-through" style:number-wrapped-paragraphs="no-limit"` +
|
||||
` style:vertical-pos="from-top" style:vertical-rel="page"` +
|
||||
` style:horizontal-pos="from-left" style:horizontal-rel="page" style:flow-with-text="false"/></style:style>`;
|
||||
const cm = (mm: number) => Math.round(mm * 10) / 100;
|
||||
const line = (name: string, mm: number, lenMm: number) =>
|
||||
`<draw:line text:anchor-type="paragraph" draw:z-index="1" draw:name="${name}" draw:style-name="FoldMark"` +
|
||||
` svg:x1="${cm(MARK_START_MM)}cm" svg:y1="${cm(mm)}cm" svg:x2="${cm(MARK_START_MM + lenMm)}cm" svg:y2="${cm(mm)}cm"/>`;
|
||||
const shapes = FOLD_MARK_MM.map((mm, i) => line(`${FOLD_MARK_NAME}${i + 1}`, mm, FOLD_MARK_LEN_MM)).join('')
|
||||
+ line(`${FOLD_MARK_NAME}Punch`, PUNCH_MARK_MM, PUNCH_MARK_LEN_MM);
|
||||
|
||||
// Same splice as the watermark: declare draw:, mint the style, ride the header —
|
||||
// or give the master page a zero-height one.
|
||||
let out = styles.includes('xmlns:draw=')
|
||||
? styles
|
||||
: styles.replace(/<office:document-styles\b/, '<office:document-styles xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"');
|
||||
out = injectAutomaticStyles(out, minted);
|
||||
if (out.includes('<style:header>')) {
|
||||
out = out.replace(/<style:header>(\s*<text:p[^>]*>)?/, (m) =>
|
||||
m.includes('<text:p') ? `${m}${shapes}` : `${m}<text:p text:style-name="Header">${shapes}</text:p>`);
|
||||
} else {
|
||||
const zone = `<style:header><text:p text:style-name="Header">${shapes}</text:p></style:header>`;
|
||||
out = out.replace(/<style:header-style\s*\/>/, '<style:header-style><style:header-footer-properties fo:min-height="0cm" fo:margin-bottom="0cm"/></style:header-style>');
|
||||
out = /<style:master-page\b[^>]*\/>/.test(out)
|
||||
? out.replace(/(<style:master-page\b[^>]*)\/>/, `$1>${zone}</style:master-page>`)
|
||||
: out.replace(/(<style:master-page\b[^>]*>)/, `$1${zone}`);
|
||||
}
|
||||
files['styles.xml'] = strToU8(out);
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// The text width the page layout declares, in cm — the watermark spans it, as
|
||||
// LibreOffice's does.
|
||||
function lengthOfPageLayout(styles: string): number {
|
||||
@@ -4500,7 +4544,7 @@ export type HfExport = {
|
||||
};
|
||||
|
||||
// The full document → .odt pipeline, DOM-free; returns the .odt bytes.
|
||||
export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAULT_MARGINS, orientation: Orientation = 'portrait', hf?: HfExport, language?: { language: string; country: string } | null, pageFormat: PageFormat = 'A4', styles: StyleSheet = builtinStyleSheet(), tabIntervalCm: number = DEFAULT_TAB_INTERVAL_CM, spacingModel: SpacingModel = 'add', rtl = false, notesSettings: NoteSettings = DEFAULT_NOTE_SETTINGS, props: DocProperties = EMPTY_DOC_PROPERTIES, hyphenate = false, pageNumbering: PageNumbering = DEFAULT_PAGE_NUMBERING, decor: PageDecor = EMPTY_PAGE_DECOR, lineNumbering: LineNumbering = DEFAULT_LINE_NUMBERING, recordChanges = false): Promise<Uint8Array> {
|
||||
export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAULT_MARGINS, orientation: Orientation = 'portrait', hf?: HfExport, language?: { language: string; country: string } | null, pageFormat: PageFormat = 'A4', styles: StyleSheet = builtinStyleSheet(), tabIntervalCm: number = DEFAULT_TAB_INTERVAL_CM, spacingModel: SpacingModel = 'add', rtl = false, notesSettings: NoteSettings = DEFAULT_NOTE_SETTINGS, props: DocProperties = EMPTY_DOC_PROPERTIES, hyphenate = false, pageNumbering: PageNumbering = DEFAULT_PAGE_NUMBERING, decor: PageDecor = EMPTY_PAGE_DECOR, lineNumbering: LineNumbering = DEFAULT_LINE_NUMBERING, recordChanges = false, foldMarks = false): Promise<Uint8Array> {
|
||||
// Images become IMG sentinels before serialization; applyImages resolves them and writes
|
||||
// the Pictures/ + manifest entries. Text boxes and columns hoist after replacePageBreaks
|
||||
// (so PGB misses their blocks) and before the inline passes (which then cover them).
|
||||
@@ -4744,7 +4788,7 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
const usedTables = new Set(tableStyleNames.filter((t): t is TableStyleRef => !!t).map(t => t.name));
|
||||
const withStyles = rewriteStylesXml(withNamedStyles, language ?? null, pageFormat, orientation, styles, usedStyleNames(docJson, styles), usedTables, tabIntervalCm, margins.mirrored === true, rtl, notesSettings, hyphenate, pageNumbering, decor, lineNumbering);
|
||||
const withHf = applyHfPostProcess(withStyles, margins, headerPara, footerPara, headerDist, footerDist, firstHeaderPara, firstFooterPara, hf?.pageCount ?? 1, hfImages, evenHeaderPara, evenFooterPara);
|
||||
const withWatermark = applyWatermarkOdf(withHf, decor.watermark);
|
||||
const withWatermark = applyFoldMarksOdf(applyWatermarkOdf(withHf, decor.watermark), foldMarks);
|
||||
// Sections past the first get their own master page, which is where ODF keeps a
|
||||
// section's header/footer; the SEC-marked block points at it.
|
||||
const withSections = applySectionMasterPages(withWatermark, hf?.sections ?? [], hf?.pageCount ?? 1, margins, pageFormat, orientation);
|
||||
|
||||
@@ -849,6 +849,8 @@ const de: Messages = {
|
||||
pageNumberContinue: 'Vom vorherigen Abschnitt fortsetzen',
|
||||
lineNumbers: 'Zeilennummern',
|
||||
pageDecor: 'Seitengestaltung',
|
||||
foldMarks: 'Falzmarken',
|
||||
foldMarksHint: 'Falz- und Lochmarken am linken Rand (DIN 5008)',
|
||||
hyphenationHint: 'Automatisch trennen',
|
||||
debugDump: 'Dump',
|
||||
cellMargins: 'Zellenränder',
|
||||
|
||||
@@ -855,6 +855,8 @@ const en = {
|
||||
pageNumberContinue: 'Continue from the previous section',
|
||||
lineNumbers: 'Line numbers',
|
||||
pageDecor: 'Page design',
|
||||
foldMarks: 'Fold marks',
|
||||
foldMarksHint: 'Fold and punch marks in the left margin (DIN 5008)',
|
||||
hyphenationHint: 'Hyphenate automatically',
|
||||
debugDump: 'Dump',
|
||||
cellMargins: 'Cell margins',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DocxStyles, parseRunProps, mergeRunProps, readNumPr, readTabStops, togg
|
||||
import { lengthToPt, WATERMARK_NAME } from './styleResolver';
|
||||
import { HEADING_STYLE_OVERRIDES, MAX_HEADING_LEVEL, normalizeColor } from '../export/odt';
|
||||
import { PLACEHOLDER_SDT_TAG } from '../export/docx';
|
||||
import { FOLD_MARK_NAME } from '../storage/foldMarks';
|
||||
import { builtinStyleSheet, DEFAULT_STYLE, type ParaProps, type Style, type StyleSheet, type TextProps } from '../styles/styleSheet';
|
||||
import { HEADER_SHADE } from '../editor/extensions/tableHeaderRow';
|
||||
import { fitInlineImage, framePx } from '../editor/extensions/image';
|
||||
@@ -263,6 +264,7 @@ export function importDocx(bytes: Uint8Array, convertedImages: ConvertedImages =
|
||||
rtl: sectPrRtl(finalSectPr),
|
||||
decor: docxPageDecor(docDoc, finalSectPr, files),
|
||||
lineNumbering: docxLineNumbering(finalSectPr),
|
||||
foldMarks: docxFoldMarks(files),
|
||||
hyphenate: docAutoHyphenation(files),
|
||||
recordChanges: docRecordsChanges(files),
|
||||
pageNumbering: docxPageNumbering(sectPr),
|
||||
@@ -2075,6 +2077,9 @@ function convertPict(pict: Element, ctx: Ctx): Node | null {
|
||||
const shape = Array.from(pict.children).find(
|
||||
(c) => c.namespaceURI === VML && ['shape', 'rect', 'oval', 'roundrect'].includes(c.localName),
|
||||
);
|
||||
// Fold-mark lines ride the flag (storage/foldMarks.ts), never the zone's content.
|
||||
const vmlLine = Array.from(pict.children).find((c) => c.namespaceURI === VML && c.localName === 'line');
|
||||
if (vmlLine?.getAttribute('id')?.startsWith(FOLD_MARK_NAME)) return null;
|
||||
if (!shape) { ctx.warnings.add('Drawings were removed'); return null; }
|
||||
// The watermark rides the page decoration (storage/pageDecor.ts), not the header's
|
||||
// content, so it must not also arrive here as a shape.
|
||||
@@ -2561,6 +2566,12 @@ function docxPageDecor(docDoc: Document, sectPr: Element | null, files: Record<s
|
||||
});
|
||||
}
|
||||
|
||||
// The export's named fold-mark lines, in whichever header part carries them.
|
||||
function docxFoldMarks(files: Record<string, Uint8Array>): boolean {
|
||||
return Object.keys(files).some((path) =>
|
||||
/^word\/header\d*\.xml$/.test(path) && strFromU8(files[path]).includes(FOLD_MARK_NAME));
|
||||
}
|
||||
|
||||
// The VML shape Word and LibreOffice both name PowerPlusWaterMarkObject, in whichever
|
||||
// header part carries it. Its text rides v:textpath, its angle the style's rotation.
|
||||
function docxWatermark(files: Record<string, Uint8Array>): unknown {
|
||||
|
||||
+12
-1
@@ -35,6 +35,7 @@ import type { IndexKind } from '../editor/extensions/tableOfContents';
|
||||
import { isBibType } from '../editor/extensions/bibliographyEntry';
|
||||
import { citationStyleFromTemplate } from '../utils/citationStyle';
|
||||
import type { PageDecor } from '../storage/pageDecor';
|
||||
import { FOLD_MARK_NAME } from '../storage/foldMarks';
|
||||
import type { LineNumbering } from '../storage/lineNumbering';
|
||||
import type { EmbeddedFont } from '../fonts/embeddedFonts';
|
||||
import { cellPaddingAttr, DEFAULT_CELL_PADDING, type CellPadding } from '../editor/extensions/tableCellPadding';
|
||||
@@ -69,6 +70,8 @@ export interface OdtImportResult {
|
||||
// Page background, page border and watermark (storage/pageDecor.ts).
|
||||
decor: PageDecor;
|
||||
lineNumbering: LineNumbering;
|
||||
// Fold + punch marks in the left margin (the export's named header lines).
|
||||
foldMarks: boolean;
|
||||
// Automatic hyphenation (ODF fo:hyphenate on the base style, Word w:autoHyphenation).
|
||||
hyphenate: boolean;
|
||||
// Whether the document records revisions (ODF text:track-changes, Word w:trackRevisions).
|
||||
@@ -160,6 +163,8 @@ type Ctx = {
|
||||
// Footnotes/endnotes in anchor order: the file stores each note's text at its anchor,
|
||||
// the editor keeps them in one section at the document end (notes.ts).
|
||||
notes: { id: string; kind: NoteKind; label: string | null; text: string; content: Node[]; styleName: string | null }[];
|
||||
// Set when a header carries the export's named fold-mark lines (storage/foldMarks.ts).
|
||||
foldMarks: boolean;
|
||||
};
|
||||
|
||||
// Read a Pictures/ entry into a base64 data-URI; null when it's missing or in a format
|
||||
@@ -630,6 +635,11 @@ function convertDrawElement(e: Element, ctx: Ctx): { inline?: Node; block?: Node
|
||||
// The watermark is not a drawing: it rides the page decoration instead
|
||||
// (storage/pageDecor.ts), so it must not also arrive as a shape in the header.
|
||||
if (e.getAttributeNS(NS.draw, 'name') === WATERMARK_NAME) return null;
|
||||
// Fold marks ride the flag (storage/foldMarks.ts), not the document, same rule.
|
||||
if (e.getAttributeNS(NS.draw, 'name')?.startsWith(FOLD_MARK_NAME)) {
|
||||
ctx.foldMarks = true;
|
||||
return null;
|
||||
}
|
||||
if (e.localName === 'frame') {
|
||||
const textBoxEl = Array.from(e.children).find(
|
||||
c => c.namespaceURI === NS.draw && c.localName === 'text-box',
|
||||
@@ -739,7 +749,7 @@ export function importOdt(bytes: Uint8Array, convertedImages: ConvertedImages =
|
||||
const contentWidthCm = geo
|
||||
? pageDimsCm(geo.format, geo.orientation).w - geo.margins.left - geo.margins.right
|
||||
: pageDimsCm('A4', 'portrait').w - 2 * 2.12;
|
||||
const ctx: Ctx = { resolver, styleNames, usedStyles: new Set(), charStyleNames, usedCharStyles: new Set(), warnings, files, imageCache: new Map(), convertedImages, pendingBlocks: [], contentWidthCm, pageRtl: geo?.rtl ?? false, masterPages: [], masterPageStarts: [], masterBlocks: new Map(), openBookmarks: new Set(), openComments: new Map(), revisions: odfRevisions(body), openInsertions: new Map(), notes: [] };
|
||||
const ctx: Ctx = { resolver, styleNames, usedStyles: new Set(), charStyleNames, usedCharStyles: new Set(), warnings, files, imageCache: new Map(), convertedImages, pendingBlocks: [], contentWidthCm, pageRtl: geo?.rtl ?? false, masterPages: [], masterPageStarts: [], masterBlocks: new Map(), openBookmarks: new Set(), openComments: new Map(), revisions: odfRevisions(body), openInsertions: new Map(), notes: [], foldMarks: false };
|
||||
let blocks = convertBlocks(Array.from(body.children), ctx, 'body');
|
||||
if (blocks.length === 0) blocks.push({ type: 'paragraph' });
|
||||
pairAlignedFrames(blocks, Math.floor(cmToPx(contentWidthCm)));
|
||||
@@ -819,6 +829,7 @@ export function importOdt(bytes: Uint8Array, convertedImages: ConvertedImages =
|
||||
rtl: geometry?.rtl ?? false,
|
||||
decor: resolver.pageDecor(),
|
||||
lineNumbering: resolver.lineNumbering(),
|
||||
foldMarks: ctx.foldMarks,
|
||||
hyphenate: resolver.documentHyphenation(),
|
||||
recordChanges: odfRecordChanges(body),
|
||||
pageNumbering: { format: resolver.pageNumberFormat(), start: odfPageNumberStart(resolver, body) },
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- **`pageDecor.ts`** — the page's own decoration (`edentext-page-decor`, the key removed when all three are off): background colour, a border around the text area at `paddingCm` from it, and a text watermark (LibreOffice's Format ▸ Watermark, field for field). All three ride the ODF page layout — the watermark as a `fontwork-plain-text` `<draw:custom-shape>` named `PowerPlusWaterMarkObject` in the **master page's header**, the only place LibreOffice reads one from (probed: hung off the master page directly it is dropped, and an undeclared `xmlns:draw` in styles.xml drops it too). Word keeps the background on `w:document` (plus `w:displayBackgroundShape`), the border in `w:sectPr` **after `w:pgMar`** — the child order is fixed, and ahead of the header references Word's reader loses those — and the watermark as the VML `_x0000_t136` shape, whose definition has to travel or LibreOffice cannot resolve it. `PageDecorLayer.svelte` draws border and watermark per page; the background is a `--color-page-bg` override on `.paper`.
|
||||
- **`trackChanges.svelte.ts`** — whether the **document** records revisions (`edentext-record-changes`, the key removed when off, as it is by default in both word processors). It travels with the file — ODF `text:track-changes` on the change registry, Word `w:trackRevisions` in settings.xml (both probed against soffice) — so an import adopts the file's flag and a new document starts off; only the open document's state lives under the key. A reactive singleton because the extension reads it on every transaction while the Review tab flips it; the recorded changes themselves live in the document, on the marks.
|
||||
- **`lineNumbering.ts`** — numbers in the left margin (`edentext-line-numbering`, the key removed when off, as it is by default in both word processors). ODF keeps one `<text:linenumbering-configuration>` in `office:styles`, Word a `<w:lnNumType>` per `w:sectPr` (probed). `LineNumberLayer.svelte` measures the lines: CSS exposes no line boxes, so a Range per block is taken and its distinct client-rect tops are the lines.
|
||||
- **`foldMarks.ts`** — fold + punch marks in the left margin (`edentext-fold-marks`, the key removed when off; neither word processor prints any by default). The DIN 5008 positions (87/192mm folds, 148.5mm punch) are this module's constants; `FoldMarkLayer.svelte` draws them per page. They export as named lines (`EdenFoldMark*`) in the master page's header — the one place that repeats on every page, where LibreOffice's own DIN templates keep them — ODF as page-relative `<draw:line>`s, DOCX as VML `<v:line>`s; both importers read the names back into the flag and never into the zone's content.
|
||||
- **`pageOrientation.ts`** — `'portrait' | 'landscape'`. `applyOrientationVars` sets `--user-page-{width,height}`; landscape swaps the A4 dimensions (matching odf-kit's automatic swap).
|
||||
|
||||
Both margins and orientation are passed into the ODT export so the exported document's geometry/line-wrapping matches the on-screen preview.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Fold and punch marks in the left margin (DIN 5008 letter sheets). Off by default —
|
||||
// neither word processor prints any — so a fresh document writes nothing. They ride the
|
||||
// master page's header on export, like the watermark, as named <draw:line>/VML lines.
|
||||
|
||||
const KEY = 'edentext-fold-marks';
|
||||
|
||||
/** Distances from the top paper edge, in mm (DIN 5008: fold marks and punch mark). */
|
||||
export const FOLD_MARK_MM = [87, 192];
|
||||
export const PUNCH_MARK_MM = 148.5;
|
||||
|
||||
/** Tick geometry, in mm from the left paper edge; the punch mark is the longer one. */
|
||||
export const MARK_START_MM = 5;
|
||||
export const FOLD_MARK_LEN_MM = 4;
|
||||
export const PUNCH_MARK_LEN_MM = 8;
|
||||
|
||||
/** The export names its lines EdenFoldMark1/2/Punch; the importers read them back. */
|
||||
export const FOLD_MARK_NAME = 'EdenFoldMark';
|
||||
|
||||
export function loadFoldMarks(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(KEY) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveFoldMarks(on: boolean): void {
|
||||
if (!on) localStorage.removeItem(KEY);
|
||||
else localStorage.setItem(KEY, 'true');
|
||||
}
|
||||
@@ -16,6 +16,10 @@ import { importDocx } from '../src/lib/import/docx';
|
||||
import { EMPTY_HF_SET } from '../src/lib/storage/headerFooter';
|
||||
import { DEFAULT_NOTE_SETTINGS } from '../src/lib/storage/noteSettings';
|
||||
import { DEFAULT_PAGE_NUMBERING } from '../src/lib/storage/pageNumbering';
|
||||
import { EMPTY_PAGE_DECOR } from '../src/lib/storage/pageDecor';
|
||||
import { DEFAULT_LINE_NUMBERING } from '../src/lib/storage/lineNumbering';
|
||||
import { EMPTY_DOC_PROPERTIES } from '../src/lib/storage/docProperties';
|
||||
import { FOLD_MARK_NAME } from '../src/lib/storage/foldMarks';
|
||||
|
||||
type N = any;
|
||||
|
||||
@@ -2535,3 +2539,39 @@ describe('Leg 31: placeholder fields (ODT + DOCX)', () => {
|
||||
!fields.some((f) => (f.marks ?? []).some((m: N) => m.type === 'textStyle' && m.attrs?.color)), fields);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leg 32: fold marks (ODT + DOCX)', () => {
|
||||
const doc: N = { type: 'doc', content: [P(null, T('Brieftext'))] };
|
||||
const commonTail = [undefined, undefined, 'A4', builtinStyleSheet(), 1.25, 'add', false,
|
||||
DEFAULT_NOTE_SETTINGS, EMPTY_DOC_PROPERTIES, false, DEFAULT_PAGE_NUMBERING,
|
||||
EMPTY_PAGE_DECOR, DEFAULT_LINE_NUMBERING, false, true] as const;
|
||||
|
||||
it('ODT: named header lines carry the flag both ways', async () => {
|
||||
const bytes = await buildOdt(doc, margins, 'portrait', ...commonTail);
|
||||
const styles = strFromU8(unzipSync(bytes)['styles.xml']);
|
||||
check('three named lines in the header', (styles.match(new RegExp(FOLD_MARK_NAME, 'g')) ?? []).length === 3, styles.slice(0, 200));
|
||||
check('page-relative position', styles.includes('style:vertical-rel="page"'));
|
||||
const res = importOdt(bytes);
|
||||
check('flag comes back', res.foldMarks === true);
|
||||
check('no shape leaks into the header zone', res.header === null, res.header);
|
||||
check('no warning for the dropped lines', res.warnings.length === 0, res.warnings);
|
||||
// Off exports nothing and imports off.
|
||||
const plain = await buildOdt(doc, margins, 'portrait');
|
||||
check('off writes no lines', !strFromU8(unzipSync(plain)['styles.xml']).includes(FOLD_MARK_NAME));
|
||||
check('off imports off', importOdt(plain).foldMarks === false);
|
||||
});
|
||||
|
||||
it('DOCX: named VML lines carry the flag both ways', async () => {
|
||||
const bytes = await buildDocx(doc, margins, 'portrait', ...commonTail);
|
||||
const files = unzipSync(bytes);
|
||||
const headers = Object.keys(files).filter((p) => /^word\/header\d*\.xml$/.test(p));
|
||||
check('a header part exists for the lines', headers.length > 0, Object.keys(files));
|
||||
check('the lines ride a header', headers.some((p) => strFromU8(files[p]).includes(FOLD_MARK_NAME)));
|
||||
const res = importDocx(bytes);
|
||||
check('flag comes back', res.foldMarks === true);
|
||||
check('no shape leaks into the header zone', res.header === null, res.header);
|
||||
check('no warning for the dropped lines', res.warnings.length === 0, res.warnings);
|
||||
const plain = await buildDocx(doc, margins, 'portrait');
|
||||
check('off imports off', importDocx(plain).foldMarks === false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user