nuvra 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,11 @@ export declare const singleUrl: (content: TransferContent) => string | null;
17
17
  export declare const textToFragment: (text: string) => DocumentFragment;
18
18
  /** Converts a payload into editor blocks, preferring sanitised HTML over plain text; `null` when it has neither. */
19
19
  export declare const transferToFragment: (content: TransferContent) => DocumentFragment | null;
20
+ /** Clean HTML and plain text of a selection, as put on the clipboard by copy, cut and drag. */
21
+ export declare const selectionClipboardData: (root: HTMLElement, range: Range) => {
22
+ html: string;
23
+ text: string;
24
+ };
20
25
  /** Puts clean HTML and plain text of the selection on a clipboard or drag payload for copy, cut and drag. */
21
26
  export declare const writeTransfer: (data: DataTransfer, root: HTMLElement, range: Range) => void;
22
27
  /** Collapsed range at the text position under the pointer, used to place dropped content and canvas clicks. */
@@ -1,8 +1,8 @@
1
1
  import { type EditorUiState } from '../ui-state';
2
- import { type HeadingTag, type TextAlign, type TextDirection } from './blocks';
2
+ import { type HeadingTag, type ParagraphIndents, type SpacingSide, type TextAlign, type TextDirection } from './blocks';
3
3
  import { EditorHistory } from './history';
4
4
  import { type ListKind } from './lists';
5
- import { type MarkName, type StyleName } from './marks';
5
+ import { type MarkName, type StyleName, type TextCase } from './marks';
6
6
  import { SearchController, type SearchState } from './search';
7
7
  import { type TableCell } from './tables';
8
8
  /** Settings the host component passes when it creates the engine. */
@@ -46,6 +46,23 @@ interface ImageAttributes {
46
46
  /** Rendered height in CSS pixels. */
47
47
  height?: number | null;
48
48
  }
49
+ /** Character and paragraph formatting picked up by the format painter. */
50
+ export interface CopiedFormat {
51
+ /** Marks that were active on the copied text. */
52
+ marks: Partial<Record<MarkName, boolean>>;
53
+ /** Span styles of the copied text; empty values mean the document default. */
54
+ styles: Partial<Record<StyleName, string>>;
55
+ /** Highlight colour of the copied text, empty when it was not highlighted. */
56
+ highlight: string;
57
+ /** Tag of the copied block, so headings are painted as headings. */
58
+ tag: 'P' | HeadingTag;
59
+ /** Alignment of the copied paragraph. */
60
+ align: TextAlign;
61
+ /** Line spacing of the copied paragraph, empty for the default. */
62
+ lineHeight: string;
63
+ /** Indentation steps of the copied paragraph. */
64
+ indent: number;
65
+ }
49
66
  /** Callback registered with {@link DocumentEngine.on}. */
50
67
  type Listener<T> = (payload: T) => void;
51
68
  /**
@@ -81,6 +98,8 @@ export declare class DocumentEngine {
81
98
  private dragRange;
82
99
  /** Image selected by a click, shown with resize handles. */
83
100
  private selectedImageElement;
101
+ /** Formatting the format painter carries until it is painted onto a selection. */
102
+ private painterFormat;
84
103
  /**
85
104
  * Takes over `root` as the editable document, loads the initial content and starts listening to its events.
86
105
  *
@@ -140,6 +159,14 @@ export declare class DocumentEngine {
140
159
  * @param position Place the caret at the start or end; without it the last selection is restored.
141
160
  */
142
161
  focus(position?: 'start' | 'end'): void;
162
+ /** Selects the whole document, as Ctrl/⌘+A does. */
163
+ selectAll(): void;
164
+ /** Copies the selection to the system clipboard as clean HTML and plain text. */
165
+ copySelection(): Promise<void>;
166
+ /** Copies the selection and removes it as one undo step. */
167
+ cutSelection(): Promise<void>;
168
+ /** Inserts the clipboard content at the selection; the browser asks for clipboard permission the first time. */
169
+ pasteFromClipboard(): Promise<void>;
143
170
  /** Marks the next selection changes as caused by the engine, so they keep pending formatting and typing groups. */
144
171
  private expectInternalSelection;
145
172
  /** Live selection inside the document, or a copy of the last one while focus is elsewhere (e.g. in a popover). */
@@ -248,6 +275,19 @@ export declare class DocumentEngine {
248
275
  setHighlight(color: string | null): void;
249
276
  /** Merges a formatting change into the pending format and keeps focus in the document. */
250
277
  private setPending;
278
+ /** Changes the letter case of the selected text. */
279
+ changeTextCase(mode: TextCase): void;
280
+ /** Stores the format the painter carries and shows the painting cursor while it does. */
281
+ private setPainterFormat;
282
+ /**
283
+ * Picks up the character and paragraph formatting at the selection. The next selection made with the mouse, or a
284
+ * click inside a paragraph, receives it; {@link cancelFormatPainter} and Escape drop it again.
285
+ */
286
+ copyFormat(): void;
287
+ /** Drops the copied format without painting it. */
288
+ cancelFormatPainter(): void;
289
+ /** Paints the copied format onto the selection, or onto the whole paragraph at a collapsed caret. */
290
+ paintFormat(): void;
251
291
  /** Removes marks from the selection and turns its blocks into plain paragraphs outside lists and quotes. */
252
292
  clearFormatting(): void;
253
293
  /** Turns the selected blocks into paragraphs or headings. */
@@ -260,6 +300,10 @@ export declare class DocumentEngine {
260
300
  setTextAlign(align: TextAlign): void;
261
301
  /** Sets or removes (`null`) the line spacing of the selected paragraphs. */
262
302
  setLineHeight(lineHeight: string | null): void;
303
+ /** Sets exact indents on the selected paragraphs, in pixels; sides left out keep their value. */
304
+ setParagraphIndents(patch: Partial<ParagraphIndents>): void;
305
+ /** Sets the space before or after the selected paragraphs, in points; `null` restores the document default. */
306
+ setParagraphSpacing(side: SpacingSide, points: number | null): void;
263
307
  /** Sets the writing direction of the selected paragraphs. */
264
308
  setTextDirection(direction: TextDirection): void;
265
309
  /** Turns the selected blocks into a list of the given kind, converts another list kind, or lifts them out. */
@@ -37,6 +37,15 @@ export declare const setLink: (root: HTMLElement, range: Range, href: string, ta
37
37
  export declare const unsetLink: (root: HTMLElement, range: Range) => void;
38
38
  /** Removes every mark, span style, highlight and link from the selected text. */
39
39
  export declare const clearMarks: (root: HTMLElement, range: Range) => void;
40
+ /** Letter case transformations offered by the toolbar. */
41
+ export type TextCase = 'upper' | 'lower' | 'title' | 'sentence' | 'toggle';
42
+ /**
43
+ * Changes the letter case of the selected text. The selection is transformed as one string, so sentence and title
44
+ * case also work across formatting boundaries.
45
+ *
46
+ * @returns whether any text was selected.
47
+ */
48
+ export declare const applyTextCase: (root: HTMLElement, range: Range, mode: TextCase) => boolean;
40
49
  /** Which marks apply to `node`. */
41
50
  export declare const readMarks: (root: HTMLElement, node: Node) => Record<MarkName, boolean>;
42
51
  /** Value of a span style at `node`, or an empty string when none is set. */
@@ -2,6 +2,10 @@
2
2
  export declare const GAP_ATTRIBUTE = "data-doc-gap";
3
3
  /** Class marking table cells in a drag selection; editor-only. */
4
4
  export declare const SELECTED_CELL_CLASS = "doc-cell-selected";
5
+ /** Attribute holding the space a paragraph keeps before it, in points. */
6
+ export declare const SPACE_BEFORE_ATTRIBUTE = "data-space-before";
7
+ /** Attribute holding the space a paragraph keeps after it, in points. */
8
+ export declare const SPACE_AFTER_ATTRIBUTE = "data-space-after";
5
9
  /** Whether the node is a block in the editor model. */
6
10
  export declare const isBlockNode: (node: Node | null | undefined) => node is HTMLElement;
7
11
  /** Sets a paragraph's indentation level as `data-indent` plus the matching left margin, removing both at level 0. */
@@ -1,17 +1,27 @@
1
1
  import { type PageSettings } from './page';
2
2
  /** Everything needed to print or export the document. */
3
3
  interface DocumentSnapshot {
4
- /** Clean document HTML. */
4
+ /** Clean document HTML, used when the document is not laid out in sheets. */
5
5
  html: string;
6
+ /**
7
+ * Clean HTML of every sheet, in document order. The page view passes it so printing repeats the header and footer
8
+ * with real page numbers and breaks the pages exactly where the editor shows them.
9
+ */
10
+ pages?: string[];
6
11
  /** Document title, used for the print title and file names. */
7
12
  title: string;
8
- /** Page size, orientation and margins. */
13
+ /** Page size, orientation, margins and the running texts. */
9
14
  page: PageSettings;
10
15
  }
11
16
  /** Turns a document title into a safe file name without extension. */
12
17
  export declare const toFileName: (title: string) => string;
13
- /** Standalone HTML with the page size and margins encoded as `@page`, used for printing and HTML export. */
14
- export declare const buildPrintableHtml: ({ html, title, page }: DocumentSnapshot) => string;
18
+ /**
19
+ * Standalone HTML with the page size, margins and running texts encoded in CSS, used for printing and HTML export.
20
+ * With the sheets of the page view every page carries its own header and footer, so the page numbers are exact and
21
+ * the pages break where the editor shows them; without them the document flows and the browser repeats one fixed
22
+ * header and footer on every page.
23
+ */
24
+ export declare const buildPrintableHtml: ({ html, pages, title, page }: DocumentSnapshot) => string;
15
25
  /** Word opens HTML saved with the Office namespaces as a regular document in print layout. */
16
26
  export declare const buildWordHtml: ({ html, title, page }: DocumentSnapshot) => string;
17
27
  /** Offers text content as a file download. */
@@ -3,7 +3,7 @@
3
3
  * from Lucide 1.44.0 (https://lucide.dev, ISC license, see LICENSE); the data is generated, not edited by hand.
4
4
  */
5
5
  /** Name of an icon in the editor's set. */
6
- export type IconName = 'between-horizontal-end' | 'between-horizontal-start' | 'between-vertical-end' | 'between-vertical-start' | 'bold' | 'calendar-days' | 'case-sensitive' | 'chevron-down' | 'chevron-right' | 'chevron-up' | 'code' | 'columns-2' | 'ellipsis' | 'external-link' | 'file-code' | 'file-down' | 'file-sliders' | 'file-text' | 'file-type' | 'grid-2x2-x' | 'grid-3x3' | 'highlighter' | 'image-plus' | 'italic' | 'languages' | 'link' | 'list' | 'list-indent-decrease' | 'list-indent-increase' | 'list-ordered' | 'list-todo' | 'loader-circle' | 'maximize-2' | 'minimize-2' | 'minus' | 'move-horizontal' | 'omega' | 'panel-top' | 'pencil' | 'pilcrow-left' | 'pilcrow-right' | 'plus' | 'printer' | 'quote' | 'rectangle-horizontal' | 'rectangle-vertical' | 'redo-2' | 'remove-formatting' | 'rows-2' | 'scroll-text' | 'search' | 'separator-horizontal' | 'square-code' | 'square-split-vertical' | 'strikethrough' | 'subscript' | 'superscript' | 'table-cells-merge' | 'table-cells-split' | 'text-align-center' | 'text-align-end' | 'text-align-justify' | 'text-align-start' | 'text-cursor-input' | 'trash' | 'underline' | 'undo-2' | 'unfold-vertical' | 'unlink' | 'upload' | 'whole-word' | 'x';
6
+ export type IconName = 'a-arrow-down' | 'a-arrow-up' | 'between-horizontal-end' | 'between-horizontal-start' | 'between-vertical-end' | 'between-vertical-start' | 'bold' | 'calendar-days' | 'case-sensitive' | 'chevron-down' | 'chevron-right' | 'chevron-up' | 'clipboard' | 'code' | 'columns-2' | 'copy' | 'ellipsis' | 'external-link' | 'file-code' | 'file-down' | 'file-sliders' | 'file-text' | 'file-type' | 'grid-2x2-x' | 'grid-3x3' | 'highlighter' | 'image-plus' | 'italic' | 'languages' | 'link' | 'list' | 'list-indent-decrease' | 'list-indent-increase' | 'list-ordered' | 'list-todo' | 'loader-circle' | 'maximize-2' | 'minimize-2' | 'minus' | 'move-horizontal' | 'omega' | 'paintbrush' | 'panel-top' | 'panel-top-dashed' | 'pencil' | 'pilcrow' | 'pilcrow-left' | 'pilcrow-right' | 'plus' | 'printer' | 'quote' | 'rectangle-horizontal' | 'rectangle-vertical' | 'redo-2' | 'remove-formatting' | 'rows-2' | 'ruler' | 'scissors' | 'scroll-text' | 'search' | 'separator-horizontal' | 'square-code' | 'square-split-vertical' | 'strikethrough' | 'subscript' | 'superscript' | 'table-cells-merge' | 'table-cells-split' | 'text-align-center' | 'text-align-end' | 'text-align-justify' | 'text-align-start' | 'text-cursor-input' | 'trash' | 'underline' | 'undo-2' | 'unfold-vertical' | 'unlink' | 'upload' | 'whole-word' | 'x';
7
7
  /** One SVG element of an icon: its tag and attributes. */
8
8
  export type IconElement = readonly [tag: string, attributes: Readonly<Record<string, string | number>>];
9
9
  /** Elements of every icon, by name. */
@@ -8,14 +8,23 @@ declare const MESSAGES: {
8
8
  readonly 'editor.apply': "Qo‘llash";
9
9
  readonly 'editor.bold': "Qalin";
10
10
  readonly 'editor.bulletList': "Belgili ro‘yxat";
11
+ readonly 'editor.case.lower': "kichik harflar";
12
+ readonly 'editor.case.sentence': "Gap boshi bosh harf";
13
+ readonly 'editor.case.title': "Har So‘z Bosh Harf";
14
+ readonly 'editor.case.toggle': "rEGISTRNI ALMASHTIRISH";
15
+ readonly 'editor.case.upper': "BOSH HARFLAR";
16
+ readonly 'editor.changeCase': "Harf registri";
11
17
  readonly 'editor.clearFormatting': "Formatni tozalash";
12
18
  readonly 'editor.close': "Yopish";
13
19
  readonly 'editor.codeBlock': "Kod bloki";
14
20
  readonly 'editor.colors.automatic': "Avtomatik";
15
21
  readonly 'editor.colors.custom': "Boshqa rang…";
16
22
  readonly 'editor.colors.none': "Rangsiz";
23
+ readonly 'editor.copy': "Nusxalash";
24
+ readonly 'editor.cut': "Kesish";
17
25
  readonly 'editor.defaultFont': "Standart shrift";
18
26
  readonly 'editor.defaultLineHeight': "Standart";
27
+ readonly 'editor.decreaseFontSize': "Shriftni kichraytirish";
19
28
  readonly 'editor.defaultSize': "Standart o‘lcham";
20
29
  readonly 'editor.delete': "O‘chirish";
21
30
  readonly 'editor.direction.auto': "Avtomatik yo‘nalish";
@@ -30,6 +39,21 @@ declare const MESSAGES: {
30
39
  readonly 'editor.fitWidth': "Kenglikka moslash";
31
40
  readonly 'editor.fontFamily': "Shrift";
32
41
  readonly 'editor.fontSize': "Shrift o‘lchami";
42
+ readonly 'editor.footer': "Pastki kolontitul";
43
+ readonly 'editor.formatPainter': "Format bo‘yoqchasi";
44
+ readonly 'editor.formattingMarks': "Formatlash belgilari";
45
+ readonly 'editor.header': "Yuqori kolontitul";
46
+ readonly 'editor.headerFooter': "Kolontitullar";
47
+ readonly 'editor.headerFooter.center': "Markaz";
48
+ readonly 'editor.headerFooter.clear': "Tozalash";
49
+ readonly 'editor.headerFooter.hint': "Matn har bir sahifada takrorlanadi.";
50
+ readonly 'editor.headerFooter.left': "Chap";
51
+ readonly 'editor.headerFooter.right': "O‘ng";
52
+ readonly 'editor.headerFooter.token.date': "Sana";
53
+ readonly 'editor.headerFooter.token.page': "Sahifa raqami";
54
+ readonly 'editor.headerFooter.token.pages': "Sahifalar soni";
55
+ readonly 'editor.headerFooter.token.title': "Hujjat nomi";
56
+ readonly 'editor.headerFooter.tokens': "Qo‘shish:";
33
57
  readonly 'editor.heading': "Sarlavha {level}";
34
58
  readonly 'editor.highlight': "Belgilash rangi";
35
59
  readonly 'editor.horizontalRule': "Gorizontal chiziq";
@@ -39,6 +63,7 @@ declare const MESSAGES: {
39
63
  readonly 'editor.imageSizeError': "Rasm hajmi {size} MB dan oshmasligi kerak";
40
64
  readonly 'editor.imageTypeError': "Faqat rasm fayllarini qo‘shish mumkin";
41
65
  readonly 'editor.imageUploadError': "Rasmni yuklab bo‘lmadi";
66
+ readonly 'editor.increaseFontSize': "Shriftni kattalashtirish";
42
67
  readonly 'editor.indent': "Chekinishni oshirish";
43
68
  readonly 'editor.inlineCode': "Kod";
44
69
  readonly 'editor.insert': "Qo‘shish";
@@ -52,6 +77,7 @@ declare const MESSAGES: {
52
77
  readonly 'editor.linkUrl': "Manzil (URL)";
53
78
  readonly 'editor.more': "Yana";
54
79
  readonly 'editor.moreFormatting': "Boshqa formatlar";
80
+ readonly 'editor.openLink': "Havolani ochish";
55
81
  readonly 'editor.or': "yoki havola orqali";
56
82
  readonly 'editor.orderedList': "Raqamli ro‘yxat";
57
83
  readonly 'editor.outdent': "Chekinishni kamaytirish";
@@ -70,9 +96,14 @@ declare const MESSAGES: {
70
96
  readonly 'editor.page.orientation': "Yo‘nalish";
71
97
  readonly 'editor.page.portrait': "Kitob";
72
98
  readonly 'editor.page.setup': "Sahifa sozlamalari";
99
+ readonly 'editor.page.watermark': "Suv belgisi";
100
+ readonly 'editor.page.watermarkColor': "Rangi";
101
+ readonly 'editor.page.watermarkDiagonal': "Qiya";
102
+ readonly 'editor.page.watermarkText': "Matn, masalan NUSXA";
73
103
  readonly 'editor.page.size': "Qog‘oz o‘lchami";
74
104
  readonly 'editor.pageBreak': "Sahifa uzilishi";
75
105
  readonly 'editor.paragraph': "Oddiy matn";
106
+ readonly 'editor.paste': "Joylashtirish";
76
107
  readonly 'editor.placeholder': "Hujjat matnini kiriting…";
77
108
  readonly 'editor.print': "Chop etish / PDF";
78
109
  readonly 'editor.quote': "Iqtibos";
@@ -81,13 +112,24 @@ declare const MESSAGES: {
81
112
  readonly 'editor.replace.one': "Almashtirish";
82
113
  readonly 'editor.replace.placeholder': "Almashtirish";
83
114
  readonly 'editor.replace.toggle': "Almashtirishni ko‘rsatish";
115
+ readonly 'editor.ruler': "Chizg‘ich";
116
+ readonly 'editor.ruler.firstLine': "Birinchi qator chekinishi";
117
+ readonly 'editor.ruler.indentLeft': "Chap chekinish";
118
+ readonly 'editor.ruler.indentRight': "O‘ng chekinish";
119
+ readonly 'editor.ruler.marginLeft': "Chap hoshiya";
120
+ readonly 'editor.ruler.marginRight': "O‘ng hoshiya";
84
121
  readonly 'editor.search.caseSensitive': "Katta-kichik harfni farqlash";
85
122
  readonly 'editor.search.next': "Keyingisi";
86
123
  readonly 'editor.search.placeholder': "Qidirish";
87
124
  readonly 'editor.search.previous': "Oldingisi";
88
125
  readonly 'editor.search.title': "Qidirish va almashtirish";
89
126
  readonly 'editor.search.wholeWord': "Butun so‘z";
127
+ readonly 'editor.selectAll': "Barchasini tanlash";
90
128
  readonly 'editor.source': "HTML kod";
129
+ readonly 'editor.spacing.addAfter': "Keyin bo‘sh joy qo‘shish";
130
+ readonly 'editor.spacing.addBefore': "Oldin bo‘sh joy qo‘shish";
131
+ readonly 'editor.spacing.removeAfter': "Keyingi bo‘sh joyni olib tashlash";
132
+ readonly 'editor.spacing.removeBefore': "Oldingi bo‘sh joyni olib tashlash";
91
133
  readonly 'editor.specialCharacters': "Maxsus belgilar";
92
134
  readonly 'editor.status.characters': "{count} belgi";
93
135
  readonly 'editor.status.page': "Sahifa {current} / {total}";
@@ -15,6 +15,24 @@ export interface PageMargins {
15
15
  /** Left margin. */
16
16
  left: number;
17
17
  }
18
+ /** Text repeated in the top or bottom margin of every page, in three aligned parts as in office suites. */
19
+ export interface PageHeaderFooter {
20
+ /** Left-aligned part. */
21
+ left: string;
22
+ /** Centred part. */
23
+ center: string;
24
+ /** Right-aligned part. */
25
+ right: string;
26
+ }
27
+ /** Text drawn behind the content of every page, such as “DRAFT” or “COPY”. */
28
+ export interface PageWatermark {
29
+ /** Text of the watermark; without it the page has none. */
30
+ text: string;
31
+ /** Colour of the text; it is drawn faintly, so a mid grey suits most documents. */
32
+ color: string;
33
+ /** Whether the text runs diagonally across the page instead of straight across it. */
34
+ diagonal: boolean;
35
+ }
18
36
  /** Page setup chosen by the user; the host can persist it with `v-model:page`. */
19
37
  export interface PageSettings {
20
38
  /** Paper format. */
@@ -23,6 +41,12 @@ export interface PageSettings {
23
41
  orientation: PageOrientation;
24
42
  /** Margins in millimetres. */
25
43
  margins: PageMargins;
44
+ /** Text repeated in the top margin of every page; omitted while the document has no header. */
45
+ header?: PageHeaderFooter;
46
+ /** Text repeated in the bottom margin of every page; omitted while the document has no footer. */
47
+ footer?: PageHeaderFooter;
48
+ /** Watermark drawn behind the text of every page; omitted while the document has none. */
49
+ watermark?: PageWatermark;
26
50
  }
27
51
  /** Page geometry in CSS pixels, ready for layout. */
28
52
  export interface PageMetrics {
@@ -102,6 +126,33 @@ export declare const ZOOM_MAX = 200;
102
126
  export declare const ZOOM_STEP = 10;
103
127
  /** Default page setup: portrait A4 with normal margins. */
104
128
  export declare const createPageSettings: () => PageSettings;
129
+ /** An empty header or footer, used when the user opens the form for the first time. */
130
+ export declare const createHeaderFooter: () => PageHeaderFooter;
131
+ /** Whether a header or footer holds any text at all. */
132
+ export declare const hasHeaderFooterText: (value: PageHeaderFooter | undefined) => boolean;
133
+ /** Tokens a header or footer text may contain, replaced when the text is drawn on a page. */
134
+ export declare const HEADER_FOOTER_TOKENS: readonly ["page", "pages", "date", "title"];
135
+ /** One of the tokens a header or footer text may contain. */
136
+ export type HeaderFooterToken = (typeof HEADER_FOOTER_TOKENS)[number];
137
+ /** Values the tokens of a header or footer are replaced with. */
138
+ export interface HeaderFooterContext {
139
+ /** Number of the page the text is drawn on. */
140
+ page: number;
141
+ /** Number of pages in the document. */
142
+ pages: number;
143
+ /** Document title. */
144
+ title: string;
145
+ }
146
+ /** Replaces the `{page}`, `{pages}`, `{date}` and `{title}` tokens of a header or footer text. */
147
+ export declare const renderHeaderFooter: (text: string, context: HeaderFooterContext) => string;
148
+ /** An empty watermark in the default colour, used when the user opens the form for the first time. */
149
+ export declare const createWatermark: () => PageWatermark;
150
+ /** Whether a watermark has any text to draw. */
151
+ export declare const hasWatermarkText: (value: PageWatermark | undefined) => boolean;
152
+ /** Angle the diagonal watermark is rotated by, in degrees. */
153
+ export declare const WATERMARK_ANGLE = -35;
154
+ /** Font size in pixels at which a watermark text spans the paper, straight across it or diagonally. */
155
+ export declare const watermarkFontSize: (width: number, height: number, text: string, diagonal: boolean) => number;
105
156
  /** Converts page settings into pixel geometry; an unknown paper size falls back to A4. */
106
157
  export declare const getPageMetrics: ({ size, orientation, margins }: PageSettings) => PageMetrics;
107
158
  /** Whether two margin sets are equal, used to highlight the matching preset. */
@@ -15,6 +15,12 @@ interface PaginationOptions {
15
15
  /** Whether an IME composition is in progress, during which blocks must not move. */
16
16
  isComposing: () => boolean;
17
17
  }
18
+ /**
19
+ * Splits the laid-out document into the clean HTML of every sheet, for printing and export. Pagination has already
20
+ * moved every block onto its page, so a block belongs to the page its top falls on; blocks taller than a page stay
21
+ * whole, exactly as they are shown on screen.
22
+ */
23
+ export declare const splitIntoPages: (root: HTMLElement, metrics: PageMetrics, pageCount: number) => string[];
18
24
  /**
19
25
  * Keeps the document laid out on sheets. Layout passes are batched to one per animation frame and re-run when the
20
26
  * document resizes, images or fonts load, or the host schedules one after an edit.
@@ -1,3 +1,3 @@
1
1
  /** Uploads an image and resolves with its public URL. Without a handler, images are embedded as data URLs. */
2
2
  export type DocumentImageUploadHandler = (file: File) => Promise<string>;
3
- export type DocumentMenuAction = 'source' | 'print' | 'exportHtml' | 'exportWord' | 'fullscreen';
3
+ export type DocumentMenuAction = 'source' | 'print' | 'exportHtml' | 'exportWord' | 'formattingMarks' | 'ruler' | 'fullscreen';
@@ -1,4 +1,4 @@
1
- import type { TextAlign, TextDirection } from './engine/blocks';
1
+ import { type TextAlign, type TextDirection } from './engine/blocks';
2
2
  import { type PendingFormat } from './engine/marks';
3
3
  /**
4
4
  * Flat snapshot of everything the toolbar renders. Components receive this instead of the engine so they
@@ -13,6 +13,16 @@ export interface EditorUiState {
13
13
  fontSize: string;
14
14
  /** Line spacing of the current paragraph, empty for the default. */
15
15
  lineHeight: string;
16
+ /** Space before the current paragraph in points; `0` when it uses the document default. */
17
+ spaceBefore: number;
18
+ /** Space after the current paragraph in points; `0` when it uses the document default. */
19
+ spaceAfter: number;
20
+ /** Left indent of the current paragraph in pixels, as shown by the ruler. */
21
+ indentLeft: number;
22
+ /** Right indent of the current paragraph in pixels. */
23
+ indentRight: number;
24
+ /** First line indent of the current paragraph in pixels; negative values hang the line out. */
25
+ indentFirstLine: number;
16
26
  /** Text color at the selection, empty for the default. */
17
27
  color: string;
18
28
  /** Highlight color at the selection, empty when not highlighted. */
@@ -51,6 +61,8 @@ export interface EditorUiState {
51
61
  canUndo: boolean;
52
62
  /** Whether there is a redo step. */
53
63
  canRedo: boolean;
64
+ /** Whether the format painter carries a copied format and waits for the text to paint. */
65
+ formatPainter: boolean;
54
66
  }
55
67
  /** Everything {@link readUiState} needs to know about the editor. */
56
68
  interface UiStateSource {
@@ -0,0 +1,117 @@
1
+ import { c as e, d as t, f as n, l as r, r as i } from "./page-DL7Oj2o8.js";
2
+ //#region src/styles/document-content.css?inline
3
+ var a = ".doc-content{color:#1f2328;white-space:pre-wrap;overflow-wrap:break-word;tab-size:4;font-family:Times New Roman,Times,serif;font-size:12pt;line-height:1.5}.doc-content :is(p,h1,h2,h3,h4,h5,h6):not([dir]){unicode-bidi:plaintext}.doc-content p{margin:0 0 6pt}.doc-content h1,.doc-content h2,.doc-content h3,.doc-content h4,.doc-content h5,.doc-content h6{margin:0 0 6pt;padding-top:6pt;font-weight:700;line-height:1.25}.doc-content h1{font-size:20pt}.doc-content h2{font-size:16pt}.doc-content h3{font-size:14pt}.doc-content h4{font-size:12pt}.doc-content h5{font-size:11pt}.doc-content h6{font-size:10pt}.doc-content ul,.doc-content ol{margin:0 0 6pt;padding-left:24pt}.doc-content li>p{margin-bottom:2pt}.doc-content ul[data-type=taskList]{padding-left:2pt;list-style:none}.doc-content ul[data-type=taskList] li{align-items:flex-start;gap:6pt;display:flex}.doc-content ul[data-type=taskList] li>label{-webkit-user-select:none;user-select:none;flex:none;margin-top:.3em}.doc-content ul[data-type=taskList] li>div{flex:auto;min-width:0}.doc-content ul[data-type=taskList] li[data-checked=true]>div{color:#667085;text-decoration:line-through}.doc-content blockquote{color:#475467;border-left:3px solid #b3c5f5;margin:0 0 6pt;padding:2pt 0 2pt 12pt}.doc-content blockquote>p:last-child,.doc-content li>p:last-child,.doc-content td>p:last-child,.doc-content th>p:last-child{margin-bottom:0}.doc-content pre{white-space:pre-wrap;background:#f4f6f8;border-radius:4px;margin:0 0 6pt;padding:8pt 10pt;font-family:Consolas,Courier New,monospace;font-size:10pt;line-height:1.45}.doc-content code{background:#eef1f4;border-radius:3px;padding:1px 4px;font-family:Consolas,Courier New,monospace;font-size:.9em}.doc-content pre code{font-size:inherit;background:0 0;padding:0}.doc-content hr{border:0;border-bottom:1px solid #c4cad4;height:0;margin:0 0 6pt;padding-top:6pt}.doc-content a{color:#1d4ed8;text-decoration:underline}.doc-content mark{color:inherit;border-radius:2px;padding:0 1px}.doc-content img{max-width:100%;height:auto}.doc-content img[data-align=left]{margin:0 auto 6pt 0;display:block}.doc-content img[data-align=center]{margin:0 auto 6pt;display:block}.doc-content img[data-align=right]{margin:0 0 6pt auto;display:block}.doc-content table{border-collapse:collapse;table-layout:fixed;width:100%;margin:0 0 6pt}.doc-content td,.doc-content th{box-sizing:border-box;vertical-align:top;border:1px solid #8f99a8;min-width:1em;padding:3pt 5pt}.doc-content th{text-align:left;background:#f2f4f7;font-weight:700}.doc-content .doc-page-break{break-after:page;page-break-after:always;height:0}", o = "", s = {
4
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;",
7
+ "\"": "&quot;",
8
+ "'": "&#39;"
9
+ }, c = /<div[^>]*data-type="page-break"[^>]*><\/div>/g, l = "<br clear=\"all\" style=\"page-break-before: always\">", u = /[\\/:*?"<>|]+/g, d = "document", f = 6e4, p = 4, m = 96 / 25.4, h = .16, g = [
10
+ "left",
11
+ "center",
12
+ "right"
13
+ ], _ = /(\{page\}|\{pages\})/g, v = {
14
+ position: "fixed",
15
+ left: "-10000px",
16
+ top: "0",
17
+ width: "1px",
18
+ height: "1px",
19
+ border: "0"
20
+ }, y = (e) => e.replace(/[&<>"']/g, (e) => s[e] ?? e), b = ({ size: e, orientation: t }) => {
21
+ let n = i[e] ?? i.a4, r = t === "portrait";
22
+ return {
23
+ width: r ? n.width : n.height,
24
+ height: r ? n.height : n.width
25
+ };
26
+ }, x = (e) => {
27
+ let { width: t, height: n } = b(e), { margins: r } = e;
28
+ return `size: ${t}mm ${n}mm; margin: ${r.top}mm ${r.right}mm ${r.bottom}mm ${r.left}mm;`;
29
+ }, S = (e) => Math.max(p, e / 2 - 2), C = "\n.doc-running { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; color: #475467; font: 10pt/1.4 'Times New Roman', Times, serif; }\n.doc-running span { min-width: 0; overflow: hidden; white-space: pre-wrap; }\n.doc-running span:nth-child(2) { text-align: center; }\n.doc-running span:nth-child(3) { text-align: right; }", w = (n, r, i) => !n || !e(n) ? "" : `<div class="doc-running doc-running--${r}">${g.map((e) => `<span>${y(t(n[e], i))}</span>`).join("")}</div>`, T = (e) => r(e) && e ? `<div class="doc-watermark">${y(e.text.trim())}</div>` : "", E = (e, t) => {
30
+ let { watermark: i } = e;
31
+ if (!r(i) || !i) return "";
32
+ let { width: a, height: o } = b(e), s = n(a * m, o * m, i.text, i.diagonal), c = i.diagonal ? " transform: rotate(-35deg);" : "";
33
+ return `
34
+ .doc-watermark { position: ${t ? "fixed" : "absolute"}; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; color: ${i.color}; font: 700 ${s}px/1 'Times New Roman', Times, serif; letter-spacing: 0.06em; opacity: ${h}; text-transform: uppercase; white-space: nowrap; pointer-events: none;${c} }`;
35
+ }, D = (e, t, n) => e.map((r, i) => {
36
+ let a = {
37
+ page: i + 1,
38
+ pages: e.length,
39
+ title: n
40
+ }, o = w(t.header, "header", a), s = w(t.footer, "footer", a);
41
+ return `<section class="doc-sheet">${T(t.watermark)}${o}<article class="doc-content">${r}</article>${s}</section>`;
42
+ }).join(""), O = (e) => {
43
+ let { width: t, height: n } = b(e), { margins: r } = e;
44
+ return `@page { size: ${t}mm ${n}mm; margin: 0; }
45
+ .doc-sheet { position: relative; box-sizing: border-box; width: ${t}mm; min-height: ${n}mm; padding: ${r.top}mm ${r.right}mm ${r.bottom}mm ${r.left}mm; overflow: hidden; background: #fff; break-after: page; }
46
+ .doc-sheet:last-child { break-after: auto; }
47
+ .doc-running { position: absolute; right: ${r.right}mm; left: ${r.left}mm; }
48
+ .doc-running--header { top: ${S(r.top)}mm; }
49
+ .doc-running--footer { bottom: ${S(r.bottom)}mm; }${C}${E(e, !1)}`;
50
+ }, k = (e) => {
51
+ let { margins: t } = e;
52
+ return `@page { ${x(e)} }
53
+ .doc-running { position: fixed; right: 0; left: 0; }
54
+ .doc-running--header { top: -${S(t.top)}mm; }
55
+ .doc-running--footer { bottom: -${S(t.bottom)}mm; }${C}${E(e, !0)}`;
56
+ }, A = (e) => e.replace(u, " ").trim() || d, j = ({ html: e, pages: t, title: n, page: r }) => {
57
+ let i = t !== void 0 && t.length > 0, o = {
58
+ page: 1,
59
+ pages: 1,
60
+ title: n
61
+ }, s = `${T(r.watermark)}${w(r.header, "header", o)}${w(r.footer, "footer", o)}<article class="doc-content">${e}</article>`;
62
+ return `<!doctype html>
63
+ <html lang="uz">
64
+ <head>
65
+ <meta charset="utf-8">
66
+ <title>${y(n)}</title>
67
+ <style>${i ? O(r) : k(r)} html, body { margin: 0; background: #fff; } ${a}</style>
68
+ </head>
69
+ <body>${i && t ? D(t, r, n) : s}</body>
70
+ </html>`;
71
+ }, M = (e) => `<span style='mso-field-code:${e}'>1</span>`, N = (e) => !r(e) || !e ? "" : `<!--[if gte vml 1]><v:shapetype id="_x0000_t136" coordsize="21600,21600" o:spt="136" adj="10800" path="m@7,l@8,m@5,21600l@6,21600e"/><v:shape id="NuvraWatermark" type="#_x0000_t136" style='position:absolute;margin-left:0;margin-top:0;width:468pt;height:117pt;rotation:${e.diagonal ? 315 : 0};z-index:-251658752;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin' fillcolor="${y(e.color)}" stroked="f"><v:fill opacity="${h}"/><v:textpath style='font-family:"Times New Roman";font-size:1pt' string="${y(e.text.trim())}"/></v:shape><![endif]-->`, P = (n, r, i, a = "") => {
72
+ let o = n !== void 0 && e(n);
73
+ if (!o && !a) return "";
74
+ let s = (e) => e.split(_).map((e) => e === "{page}" ? M("PAGE") : e === "{pages}" ? M("NUMPAGES") : y(t(e, {
75
+ page: 1,
76
+ pages: 1,
77
+ title: i
78
+ }))).join(""), [c, l, u] = g.map((e) => o && n ? s(n[e]) : "");
79
+ return `<div style='mso-element:${r}' id='${r === "header" ? "h1" : "f1"}'>${a}
80
+ <table width="100%" style='border-collapse:collapse'><tr>
81
+ <td style='border:none;padding:0'>${c}</td>
82
+ <td style='border:none;padding:0;text-align:center'>${l}</td>
83
+ <td style='border:none;padding:0;text-align:right'>${u}</td>
84
+ </tr></table></div>`;
85
+ }, F = ({ html: e, title: t, page: n }) => {
86
+ let r = e.replace(c, l), i = P(n.header, "header", t, N(n.watermark)), s = P(n.footer, "footer", t), u = `${i ? " mso-header: h1;" : ""}${s ? " mso-footer: f1;" : ""}`;
87
+ return `${o}<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
88
+ <head>
89
+ <meta charset="utf-8">
90
+ <title>${y(t)}</title>
91
+ <!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:Zoom>100</w:Zoom></w:WordDocument></xml><![endif]-->
92
+ <style>@page Section1 { ${x(n)}${u} } div.Section1 { page: Section1; } ${a}</style>
93
+ </head>
94
+ <body><div class="Section1 doc-content">${r}</div>${i}${s}</body>
95
+ </html>`;
96
+ }, I = (e, t, n) => {
97
+ let r = URL.createObjectURL(new Blob([e], { type: n })), i = document.createElement("a");
98
+ i.href = r, i.download = t, i.click(), setTimeout(() => URL.revokeObjectURL(r), 0);
99
+ }, L = (e) => {
100
+ let t = document.createElement("iframe");
101
+ t.setAttribute("aria-hidden", "true"), t.tabIndex = -1, Object.assign(t.style, v);
102
+ let n = () => t.remove();
103
+ t.onload = () => {
104
+ let e = t.contentWindow;
105
+ if (!e) return n();
106
+ let r = Array.from(e.document.images).filter((e) => !e.complete).map((e) => new Promise((t) => {
107
+ e.addEventListener("load", t, { once: !0 }), e.addEventListener("error", t, { once: !0 });
108
+ }));
109
+ Promise.allSettled(r).then(() => {
110
+ e.addEventListener("afterprint", () => setTimeout(n, 0), { once: !0 }), e.focus(), e.print(), setTimeout(n, f);
111
+ });
112
+ }, t.srcdoc = e, document.body.appendChild(t);
113
+ };
114
+ //#endregion
115
+ export { j as buildPrintableHtml, F as buildWordHtml, I as downloadFile, L as printHtml, A as toFileName };
116
+
117
+ //# sourceMappingURL=export-VeTfgCEp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-VeTfgCEp.js","names":[],"sources":["../src/styles/document-content.css?inline","../src/core/export.ts"],"sourcesContent":["/*\n * Typography of the document itself. Shared by the editor page, print output and Word/HTML export,\n * so every rule is plain CSS scoped to `.doc-content`. Vertical rhythm uses bottom margins only:\n * no margin collapsing keeps page layout measurements exact.\n */\n\n/* Base text. Typed spaces are content, so they are kept exactly. */\n.doc-content {\n color: #1f2328;\n font-family: \"Times New Roman\", Times, serif;\n font-size: 12pt;\n line-height: 1.5;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n tab-size: 4;\n}\n\n/* Paragraphs without an explicit direction follow their own text (Arabic and Latin in one document). */\n.doc-content :is(p, h1, h2, h3, h4, h5, h6):not([dir]) {\n unicode-bidi: plaintext;\n}\n\n/* Paragraphs and headings. */\n.doc-content p {\n margin: 0 0 6pt;\n}\n\n.doc-content h1,\n.doc-content h2,\n.doc-content h3,\n.doc-content h4,\n.doc-content h5,\n.doc-content h6 {\n margin: 0 0 6pt;\n padding-top: 6pt;\n font-weight: 700;\n line-height: 1.25;\n}\n\n.doc-content h1 {\n font-size: 20pt;\n}\n\n.doc-content h2 {\n font-size: 16pt;\n}\n\n.doc-content h3 {\n font-size: 14pt;\n}\n\n.doc-content h4 {\n font-size: 12pt;\n}\n\n.doc-content h5 {\n font-size: 11pt;\n}\n\n.doc-content h6 {\n font-size: 10pt;\n}\n\n/* Lists, including task lists with a checkbox label and a content column. */\n.doc-content ul,\n.doc-content ol {\n margin: 0 0 6pt;\n padding-left: 24pt;\n}\n\n.doc-content li > p {\n margin-bottom: 2pt;\n}\n\n.doc-content ul[data-type=\"taskList\"] {\n padding-left: 2pt;\n list-style: none;\n}\n\n.doc-content ul[data-type=\"taskList\"] li {\n display: flex;\n align-items: flex-start;\n gap: 6pt;\n}\n\n.doc-content ul[data-type=\"taskList\"] li > label {\n flex: 0 0 auto;\n margin-top: 0.3em;\n user-select: none;\n}\n\n.doc-content ul[data-type=\"taskList\"] li > div {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.doc-content ul[data-type=\"taskList\"] li[data-checked=\"true\"] > div {\n color: #667085;\n text-decoration: line-through;\n}\n\n/* Quotes. */\n.doc-content blockquote {\n margin: 0 0 6pt;\n padding: 2pt 0 2pt 12pt;\n border-left: 3px solid #b3c5f5;\n color: #475467;\n}\n\n/* The last paragraph of a container adds no space below it. */\n.doc-content blockquote > p:last-child,\n.doc-content li > p:last-child,\n.doc-content td > p:last-child,\n.doc-content th > p:last-child {\n margin-bottom: 0;\n}\n\n/* Code blocks and inline code. */\n.doc-content pre {\n margin: 0 0 6pt;\n padding: 8pt 10pt;\n border-radius: 4px;\n background: #f4f6f8;\n font-family: Consolas, \"Courier New\", monospace;\n font-size: 10pt;\n line-height: 1.45;\n white-space: pre-wrap;\n}\n\n.doc-content code {\n padding: 1px 4px;\n border-radius: 3px;\n background: #eef1f4;\n font-family: Consolas, \"Courier New\", monospace;\n font-size: 0.9em;\n}\n\n.doc-content pre code {\n padding: 0;\n background: none;\n font-size: inherit;\n}\n\n/* Horizontal rule. */\n.doc-content hr {\n height: 0;\n margin: 0 0 6pt;\n padding-top: 6pt;\n border: 0;\n border-bottom: 1px solid #c4cad4;\n}\n\n/* Inline marks. */\n.doc-content a {\n color: #1d4ed8;\n text-decoration: underline;\n}\n\n.doc-content mark {\n padding: 0 1px;\n border-radius: 2px;\n color: inherit;\n}\n\n/* Images are blocks aligned through their data-align attribute. */\n.doc-content img {\n max-width: 100%;\n height: auto;\n}\n\n.doc-content img[data-align=\"left\"] {\n display: block;\n margin: 0 auto 6pt 0;\n}\n\n.doc-content img[data-align=\"center\"] {\n display: block;\n margin: 0 auto 6pt;\n}\n\n.doc-content img[data-align=\"right\"] {\n display: block;\n margin: 0 0 6pt auto;\n}\n\n/* Tables. */\n.doc-content table {\n width: 100%;\n margin: 0 0 6pt;\n border-collapse: collapse;\n table-layout: fixed;\n}\n\n.doc-content td,\n.doc-content th {\n box-sizing: border-box;\n min-width: 1em;\n padding: 3pt 5pt;\n border: 1px solid #8f99a8;\n vertical-align: top;\n}\n\n.doc-content th {\n background: #f2f4f7;\n font-weight: 700;\n text-align: left;\n}\n\n/* Manual page break: invisible in print, where it starts a new sheet. */\n.doc-content .doc-page-break {\n height: 0;\n break-after: page;\n page-break-after: always;\n}\n","import contentCss from '../styles/document-content.css?inline';\nimport {\n type HeaderFooterContext,\n PAGE_SIZES,\n type PageHeaderFooter,\n type PageSettings,\n type PageWatermark,\n WATERMARK_ANGLE,\n hasHeaderFooterText,\n hasWatermarkText,\n renderHeaderFooter,\n watermarkFontSize\n} from './page';\n\n/** Everything needed to print or export the document. */\ninterface DocumentSnapshot {\n /** Clean document HTML, used when the document is not laid out in sheets. */\n html: string;\n /**\n * Clean HTML of every sheet, in document order. The page view passes it so printing repeats the header and footer\n * with real page numbers and breaks the pages exactly where the editor shows them.\n */\n pages?: string[];\n /** Document title, used for the print title and file names. */\n title: string;\n /** Page size, orientation, margins and the running texts. */\n page: PageSettings;\n}\n\n/** Makes Word detect UTF-8 instead of the system code page. */\nconst BYTE_ORDER_MARK = String.fromCharCode(0xfe_ff);\n/** Characters escaped when text is placed into HTML. */\nconst HTML_ESCAPES: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' };\n/** Page break markup of the editor. */\nconst PAGE_BREAK_PATTERN = /<div[^>]*data-type=\"page-break\"[^>]*><\\/div>/g;\n/** Page break markup Word understands. */\nconst WORD_PAGE_BREAK = '<br clear=\"all\" style=\"page-break-before: always\">';\n/** Characters not allowed in file names on common systems. */\nconst INVALID_FILE_NAME_CHARACTERS = /[\\\\/:*?\"<>|]+/g;\n/** File name used when the title is empty. */\nconst DEFAULT_FILE_NAME = 'document';\n/** Removes the print iframe even when the browser never reports `afterprint`. */\nconst PRINT_CLEANUP_TIMEOUT_MS = 60_000;\n/** Distance a running text keeps from the paper edge when its margin is too small to centre it in, in millimetres. */\nconst MIN_RUNNING_EDGE_MM = 4;\n/** CSS pixels per millimetre at 96 DPI, used to size the watermark for the paper. */\nconst PX_PER_MM = 96 / 25.4;\n/** How faintly the watermark is printed. */\nconst WATERMARK_OPACITY = 0.16;\n/** The three parts of a running text, in the order they are drawn. */\nconst RUNNING_PARTS = ['left', 'center', 'right'] as const;\n/** Splits a running text at the page tokens, so they can become Word fields while the rest stays escaped text. */\nconst PAGE_TOKEN_SPLIT = /(\\{page\\}|\\{pages\\})/g;\n/** Styles that keep the print iframe out of view without `display: none`, which would stop it from printing. */\nconst HIDDEN_FRAME_STYLE: Partial<CSSStyleDeclaration> = {\n position: 'fixed',\n left: '-10000px',\n top: '0',\n width: '1px',\n height: '1px',\n border: '0'\n};\n\n/** Escapes text for use inside HTML. */\nconst escapeHtml = (value: string) => value.replace(/[&<>\"']/g, character => HTML_ESCAPES[character] ?? character);\n\n/** Paper width and height in millimetres, in the chosen orientation. */\nconst paperSize = ({ size, orientation }: PageSettings) => {\n const paper = PAGE_SIZES[size] ?? PAGE_SIZES.a4;\n const portrait = orientation === 'portrait';\n return { width: portrait ? paper.width : paper.height, height: portrait ? paper.height : paper.width };\n};\n\n/** CSS declarations for `@page` with the paper size and margins of the page settings. */\nconst pageRule = (page: PageSettings) => {\n const { width, height } = paperSize(page);\n const { margins } = page;\n return `size: ${width}mm ${height}mm; margin: ${margins.top}mm ${margins.right}mm ${margins.bottom}mm ${margins.left}mm;`;\n};\n\n/** Distance of a running text from the top or bottom edge of the paper, centred in its margin. */\nconst runningEdge = (margin: number) => Math.max(MIN_RUNNING_EDGE_MM, margin / 2 - 2);\n\n/** Shared look of the header and footer, both in print and in the exported page. */\nconst RUNNING_CSS = `\n.doc-running { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; color: #475467; font: 10pt/1.4 'Times New Roman', Times, serif; }\n.doc-running span { min-width: 0; overflow: hidden; white-space: pre-wrap; }\n.doc-running span:nth-child(2) { text-align: center; }\n.doc-running span:nth-child(3) { text-align: right; }`;\n\n/** One running text rendered for a page, or an empty string when it holds no text. */\nconst runningHtml = (\n value: PageHeaderFooter | undefined,\n place: 'header' | 'footer',\n context: HeaderFooterContext\n): string => {\n if (!value || !hasHeaderFooterText(value)) return '';\n const parts = RUNNING_PARTS.map(part => `<span>${escapeHtml(renderHeaderFooter(value[part], context))}</span>`).join(\n ''\n );\n return `<div class=\"doc-running doc-running--${place}\">${parts}</div>`;\n};\n\n/** The watermark of a page, or an empty string when the document has none. */\nconst watermarkHtml = (watermark: PageWatermark | undefined): string =>\n hasWatermarkText(watermark) && watermark\n ? `<div class=\"doc-watermark\">${escapeHtml(watermark.text.trim())}</div>`\n : '';\n\n/** Look of the watermark; it is positioned per sheet when the pages are known, and fixed when the document flows. */\nconst watermarkCss = (page: PageSettings, fixed: boolean): string => {\n const { watermark } = page;\n if (!hasWatermarkText(watermark) || !watermark) return '';\n const { width, height } = paperSize(page);\n const size = watermarkFontSize(width * PX_PER_MM, height * PX_PER_MM, watermark.text, watermark.diagonal);\n const rotation = watermark.diagonal ? ` transform: rotate(${WATERMARK_ANGLE}deg);` : '';\n return `\n.doc-watermark { position: ${fixed ? 'fixed' : 'absolute'}; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; color: ${watermark.color}; font: 700 ${size}px/1 'Times New Roman', Times, serif; letter-spacing: 0.06em; opacity: ${WATERMARK_OPACITY}; text-transform: uppercase; white-space: nowrap; pointer-events: none;${rotation} }`;\n};\n\n/** Sheets with their own header and footer, used when the editor knows where the pages break. */\nconst sheetsHtml = (pages: string[], page: PageSettings, title: string): string =>\n pages\n .map((content, index) => {\n const context: HeaderFooterContext = { page: index + 1, pages: pages.length, title };\n const header = runningHtml(page.header, 'header', context);\n const footer = runningHtml(page.footer, 'footer', context);\n const watermark = watermarkHtml(page.watermark);\n return `<section class=\"doc-sheet\">${watermark}${header}<article class=\"doc-content\">${content}</article>${footer}</section>`;\n })\n .join('');\n\n/** Page geometry of the sheet layout: the paper is the page box and every sheet carries the margins itself. */\nconst sheetCss = (page: PageSettings) => {\n const { width, height } = paperSize(page);\n const { margins } = page;\n return `@page { size: ${width}mm ${height}mm; margin: 0; }\n.doc-sheet { position: relative; box-sizing: border-box; width: ${width}mm; min-height: ${height}mm; padding: ${margins.top}mm ${margins.right}mm ${margins.bottom}mm ${margins.left}mm; overflow: hidden; background: #fff; break-after: page; }\n.doc-sheet:last-child { break-after: auto; }\n.doc-running { position: absolute; right: ${margins.right}mm; left: ${margins.left}mm; }\n.doc-running--header { top: ${runningEdge(margins.top)}mm; }\n.doc-running--footer { bottom: ${runningEdge(margins.bottom)}mm; }${RUNNING_CSS}${watermarkCss(page, false)}`;\n};\n\n/** Page geometry of the flowing layout, where the browser breaks the pages and repeats the fixed running texts. */\nconst flowCss = (page: PageSettings) => {\n const { margins } = page;\n return `@page { ${pageRule(page)} }\n.doc-running { position: fixed; right: 0; left: 0; }\n.doc-running--header { top: -${runningEdge(margins.top)}mm; }\n.doc-running--footer { bottom: -${runningEdge(margins.bottom)}mm; }${RUNNING_CSS}${watermarkCss(page, true)}`;\n};\n\n/** Turns a document title into a safe file name without extension. */\nexport const toFileName = (title: string): string =>\n title.replace(INVALID_FILE_NAME_CHARACTERS, ' ').trim() || DEFAULT_FILE_NAME;\n\n/**\n * Standalone HTML with the page size, margins and running texts encoded in CSS, used for printing and HTML export.\n * With the sheets of the page view every page carries its own header and footer, so the page numbers are exact and\n * the pages break where the editor shows them; without them the document flows and the browser repeats one fixed\n * header and footer on every page.\n */\nexport const buildPrintableHtml = ({ html, pages, title, page }: DocumentSnapshot): string => {\n const paginated = pages !== undefined && pages.length > 0;\n const context: HeaderFooterContext = { page: 1, pages: 1, title };\n const flow = `${watermarkHtml(page.watermark)}${runningHtml(page.header, 'header', context)}${runningHtml(page.footer, 'footer', context)}<article class=\"doc-content\">${html}</article>`;\n return `<!doctype html>\n<html lang=\"uz\">\n<head>\n<meta charset=\"utf-8\">\n<title>${escapeHtml(title)}</title>\n<style>${paginated ? sheetCss(page) : flowCss(page)} html, body { margin: 0; background: #fff; } ${contentCss}</style>\n</head>\n<body>${paginated && pages ? sheetsHtml(pages, page, title) : flow}</body>\n</html>`;\n};\n\n/** Word field that counts pages; Word fills it in itself, so an exported document numbers its pages correctly. */\nconst wordField = (code: 'PAGE' | 'NUMPAGES') => `<span style='mso-field-code:${code}'>1</span>`;\n\n/**\n * Watermark in the form Word writes its own watermarks in: a VML shape inside the header. Browsers skip it, because\n * it sits in a conditional comment only Word reads.\n */\nconst wordWatermark = (watermark: PageWatermark | undefined): string => {\n if (!hasWatermarkText(watermark) || !watermark) return '';\n const rotation = watermark.diagonal ? 315 : 0;\n return `<!--[if gte vml 1]><v:shapetype id=\"_x0000_t136\" coordsize=\"21600,21600\" o:spt=\"136\" adj=\"10800\" path=\"m@7,l@8,m@5,21600l@6,21600e\"/><v:shape id=\"NuvraWatermark\" type=\"#_x0000_t136\" style='position:absolute;margin-left:0;margin-top:0;width:468pt;height:117pt;rotation:${rotation};z-index:-251658752;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin' fillcolor=\"${escapeHtml(watermark.color)}\" stroked=\"f\"><v:fill opacity=\"${WATERMARK_OPACITY}\"/><v:textpath style='font-family:\"Times New Roman\";font-size:1pt' string=\"${escapeHtml(watermark.text.trim())}\"/></v:shape><![endif]-->`;\n};\n\n/** A running text for Word, where the page tokens become the Word fields Word counts itself. */\nconst wordRunning = (\n value: PageHeaderFooter | undefined,\n place: 'header' | 'footer',\n title: string,\n extra = ''\n): string => {\n const hasText = value !== undefined && hasHeaderFooterText(value);\n if (!hasText && !extra) return '';\n const render = (text: string) =>\n text\n .split(PAGE_TOKEN_SPLIT)\n .map(chunk => {\n if (chunk === '{page}') return wordField('PAGE');\n if (chunk === '{pages}') return wordField('NUMPAGES');\n return escapeHtml(renderHeaderFooter(chunk, { page: 1, pages: 1, title }));\n })\n .join('');\n const [left, center, right] = RUNNING_PARTS.map(part => (hasText && value ? render(value[part]) : ''));\n return `<div style='mso-element:${place}' id='${place === 'header' ? 'h1' : 'f1'}'>${extra}\n<table width=\"100%\" style='border-collapse:collapse'><tr>\n<td style='border:none;padding:0'>${left}</td>\n<td style='border:none;padding:0;text-align:center'>${center}</td>\n<td style='border:none;padding:0;text-align:right'>${right}</td>\n</tr></table></div>`;\n};\n\n/** Word opens HTML saved with the Office namespaces as a regular document in print layout. */\nexport const buildWordHtml = ({ html, title, page }: DocumentSnapshot): string => {\n const body = html.replace(PAGE_BREAK_PATTERN, WORD_PAGE_BREAK);\n const header = wordRunning(page.header, 'header', title, wordWatermark(page.watermark));\n const footer = wordRunning(page.footer, 'footer', title);\n const running = `${header ? ' mso-header: h1;' : ''}${footer ? ' mso-footer: f1;' : ''}`;\n return `${BYTE_ORDER_MARK}<html xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" xmlns=\"http://www.w3.org/TR/REC-html40\">\n<head>\n<meta charset=\"utf-8\">\n<title>${escapeHtml(title)}</title>\n<!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:Zoom>100</w:Zoom></w:WordDocument></xml><![endif]-->\n<style>@page Section1 { ${pageRule(page)}${running} } div.Section1 { page: Section1; } ${contentCss}</style>\n</head>\n<body><div class=\"Section1 doc-content\">${body}</div>${header}${footer}</body>\n</html>`;\n};\n\n/** Offers text content as a file download. */\nexport const downloadFile = (content: string, fileName: string, type: string): void => {\n const url = URL.createObjectURL(new Blob([content], { type }));\n const link = document.createElement('a');\n link.href = url;\n link.download = fileName;\n link.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n};\n\n/** Prints through a detached iframe so the application chrome never ends up on paper. Waits for images first. */\nexport const printHtml = (documentHtml: string): void => {\n const frame = document.createElement('iframe');\n frame.setAttribute('aria-hidden', 'true');\n frame.tabIndex = -1;\n Object.assign(frame.style, HIDDEN_FRAME_STYLE);\n\n /** Removes the iframe once printing is done. */\n const cleanup = () => frame.remove();\n frame.onload = () => {\n const printWindow = frame.contentWindow;\n if (!printWindow) return cleanup();\n const pendingImages = Array.from(printWindow.document.images)\n .filter(image => !image.complete)\n .map(\n image =>\n new Promise(resolve => {\n image.addEventListener('load', resolve, { once: true });\n image.addEventListener('error', resolve, { once: true });\n })\n );\n void Promise.allSettled(pendingImages).then(() => {\n printWindow.addEventListener('afterprint', () => setTimeout(cleanup, 0), { once: true });\n printWindow.focus();\n printWindow.print();\n setTimeout(cleanup, PRINT_CLEANUP_TIMEOUT_MS);\n });\n };\n frame.srcdoc = documentHtml;\n document.body.appendChild(frame);\n};\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  export { default as DocumentEditor } from './components/document-editor.vue';
6
6
  export type { EditorLabelKey, EditorTranslator } from './core/labels';
7
7
  export { editorMessages, setEditorTranslator } from './core/labels';
8
- export type { DocumentViewMode, PageMargins, PageOrientation, PageSettings, PageSizeKey } from './core/page';
9
- export { createPageSettings } from './core/page';
8
+ export type { DocumentViewMode, PageHeaderFooter, PageMargins, PageOrientation, PageSettings, PageSizeKey, PageWatermark } from './core/page';
9
+ export { createHeaderFooter, createPageSettings, createWatermark } from './core/page';
10
10
  export type { DocumentImageUploadHandler } from './core/types';
11
11
  export { default as Editor } from './editor.vue';