fancy-template 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {fancy_template-0.2.0 → fancy_template-0.3.0}/PKG-INFO +1 -1
- {fancy_template-0.2.0 → fancy_template-0.3.0}/SPEC.md +335 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/pyproject.toml +1 -1
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/__init__.py +5 -1
- fancy_template-0.3.0/src/fancy_template/_files.py +40 -0
- fancy_template-0.3.0/tests/test_files.py +28 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/.gitignore +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/LICENSE +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/README.md +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/profiles/dark-slide.json +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/profiles/fine-print.json +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/profiles/holy-sheet.json +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/profiles/last-word.json +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_dates.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_declarations.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_errors.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_filters.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_inspector.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_json.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_limits.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_numbers.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_profile.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_renderer.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/_scanner.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/src/fancy_template/py.typed +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/tests/template_runner.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/tests/test_conformance.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/tests/test_security.py +0 -0
- {fancy_template-0.2.0 → fancy_template-0.3.0}/tests/test_spec.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fancy-template
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Document templates for the Fancy document writers: a writer document with {{ tokens }} and JSON directives, plus a declaration of the data it needs. inspect() at save time, validate_data() before render, render() to plain writer JSON. No code execution, no includes, bounded everywhere. One spec, three runtimes, held to a shared conformance table.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Particle-Academy/fancy-template
|
|
6
6
|
Project-URL: Issues, https://github.com/Particle-Academy/fancy-template/issues
|
|
@@ -414,5 +414,340 @@ PHP `Template::validateData`, TypeScript `validateData`, Python `validate_data`)
|
|
|
414
414
|
- `preview(template, options?) -> document` -- `render` with each variable's
|
|
415
415
|
`example` (else `default`).
|
|
416
416
|
- `tokenize(text, options?) -> parts` (section 5).
|
|
417
|
+
- `inspectFile(bytes, options?)` and `renderFile(bytes, data, options?)` (section 12).
|
|
417
418
|
|
|
418
419
|
Output key order is the template's, minus directive keys.
|
|
420
|
+
|
|
421
|
+
## 12. Office files as templates
|
|
422
|
+
|
|
423
|
+
Owner's rule: **any file can be a template.** A customer's own `.docx`, `.pptx`
|
|
424
|
+
or `.xlsx` with `{{ tokens }}` typed into it is filled AS IT IS, in its own
|
|
425
|
+
text, with every other byte of the package kept. Nothing is read into a writer
|
|
426
|
+
model and written back, because a model drops what it does not carry (custom
|
|
427
|
+
styles, embedded objects, fields, comments, layout).
|
|
428
|
+
|
|
429
|
+
**Status.** PHP implements this section. TypeScript and Python expose the two
|
|
430
|
+
functions and throw `FileTemplatesNotImplementedError` (`code`
|
|
431
|
+
`FILE_NOT_IMPLEMENTED`, `operation` the function's name), never a silent
|
|
432
|
+
no-op. Shared rows and fixtures: `fixtures/files/` (section 12.10).
|
|
433
|
+
|
|
434
|
+
### 12.1 API
|
|
435
|
+
|
|
436
|
+
| | PHP | TypeScript | Python |
|
|
437
|
+
|---|---|---|---|
|
|
438
|
+
| inspect | `Template::inspectFile(string $bytes, array $options = [])` | `inspectFile(bytes: Uint8Array, options?)` | `inspect_file(data: bytes, options=None)` |
|
|
439
|
+
| render | `Template::renderFile(string $bytes, mixed $data, array $options = []) : string` | `renderFile(bytes, data, options?) : Uint8Array` | `render_file(data, values, options=None) -> bytes` |
|
|
440
|
+
|
|
441
|
+
`inspectFile` returns
|
|
442
|
+
`{ ok, format, kind, issues: [{code, path, variable?}], placeholders: [{path, token, variable?}], variables: [name] }`:
|
|
443
|
+
|
|
444
|
+
- `format`: `docx`, `pptx` or `xlsx`; `kind`: `document` or `template` (from the
|
|
445
|
+
main part's content type, 12.3). Both `null` when the file is refused.
|
|
446
|
+
- `placeholders`: every token, in order (12.5): `path` is its location, `token`
|
|
447
|
+
the token's source text from `{{` to `}}` as it reads in the joined text,
|
|
448
|
+
`variable` the dotted path (absent for a string-literal token).
|
|
449
|
+
- `variables`: the HEAD of every token path, distinct, in first-use order: the
|
|
450
|
+
names the data must supply.
|
|
451
|
+
- A refused file (any `FILE_` code) is reported as the single issue, as an
|
|
452
|
+
oversized template is (section 10), and `inspectFile` does not throw for it.
|
|
453
|
+
|
|
454
|
+
`renderFile` returns the filled file's bytes, or throws `TemplateError`. Order:
|
|
455
|
+
open and classify the package (`FILE_` codes), inspect (first issue), validate
|
|
456
|
+
the data as `validateData` does (first error, path into the data), fill.
|
|
457
|
+
|
|
458
|
+
Options: `variables` (12.2), `limits` (`tokens`, `tokenLength`, `outputBytes`,
|
|
459
|
+
and `depth` for the data, apply; the rest have nothing to bound in a file) and
|
|
460
|
+
`fileLimits` (12.8). An
|
|
461
|
+
unknown `fileLimits` key, or a value that is not a positive integer, is refused
|
|
462
|
+
as an unknown `limits` key is.
|
|
463
|
+
|
|
464
|
+
### 12.2 Declarations: a sidecar, not document properties
|
|
465
|
+
|
|
466
|
+
`options.variables` is a declaration map exactly as an envelope's `variables`
|
|
467
|
+
(section 3), checked by the same rules with the same codes and paths
|
|
468
|
+
(`DECL_INVALID` at `/variables/<name>/...`; a non-object is `TEMPLATE_SHAPE` at
|
|
469
|
+
`/variables`). With it, the file is inspected as a template is: a token whose
|
|
470
|
+
path is not declared is `TOKEN_UNDECLARED`, a token naming an `image` variable
|
|
471
|
+
is `TOKEN_IMAGE_FIELD` (an image cannot be text), data is validated with
|
|
472
|
+
`DATA_*` codes and defaults apply.
|
|
473
|
+
|
|
474
|
+
**Why a sidecar.** Custom document properties would put the declarations inside
|
|
475
|
+
the customer's file, where:
|
|
476
|
+
|
|
477
|
+
- anyone who opens it in Word sees and can edit them (File > Properties), so the
|
|
478
|
+
customer can break the template without knowing it is one;
|
|
479
|
+
- writing or updating them rewrites `docProps/custom.xml`, which is exactly the
|
|
480
|
+
kind of change to a customer's file this feature exists to avoid;
|
|
481
|
+
- they are flat strings, so a declaration would be JSON inside a property, a
|
|
482
|
+
second format to parse in three languages;
|
|
483
|
+
- some editors drop unknown custom properties on save.
|
|
484
|
+
|
|
485
|
+
The host already stores a record for every file it accepts as a template; the
|
|
486
|
+
declarations belong in that record, beside the file, where they can be versioned
|
|
487
|
+
and reused across new uploads of the same file.
|
|
488
|
+
|
|
489
|
+
**Without declarations** the names are INFERRED from the tokens, the way
|
|
490
|
+
`inspect` reports them: no token is undeclared, nothing is typed, and
|
|
491
|
+
`validateData` has no member to check (data must still be a plain JSON object:
|
|
492
|
+
`DATA_TYPE`, `DATA_TOO_DEEP`). Every token must still resolve at render: absent
|
|
493
|
+
is `TEMPLATE_MISSING`, a container is `TEMPLATE_NOT_SCALAR`, a wrong filter input
|
|
494
|
+
is `FILTER_INPUT`. A file has no `$each`, so `$index`, `$first` and `$last` are
|
|
495
|
+
`TOKEN_UNDECLARED` in both modes.
|
|
496
|
+
|
|
497
|
+
### 12.3 Recognising the package
|
|
498
|
+
|
|
499
|
+
In this order; the first failure is the answer.
|
|
500
|
+
|
|
501
|
+
1. An OLE compound file (first 8 bytes `D0 CF 11 E0 A1 B1 1A E1`) is
|
|
502
|
+
`FILE_ENCRYPTED` when its bytes contain the UTF-16LE stream name
|
|
503
|
+
`EncryptedPackage` (a password-protected OOXML file), else `FILE_UNSUPPORTED`
|
|
504
|
+
(a legacy binary `.doc`/`.xls`/`.ppt`).
|
|
505
|
+
2. The zip is indexed and checked (12.8).
|
|
506
|
+
3. `[Content_Types].xml` must be a member (exact name), else `FILE_UNSUPPORTED`.
|
|
507
|
+
It is read like every part (12.7). Each member's content type is its
|
|
508
|
+
`Override` (by part name `/` + member name, ASCII case-insensitive), else the
|
|
509
|
+
`Default` for its extension (case-insensitive), else none. Content types
|
|
510
|
+
compare case-insensitively.
|
|
511
|
+
4. A member whose type is a macro-enabled or binary main type is
|
|
512
|
+
`FILE_MACRO_ENABLED`: `application/vnd.ms-word.document.macroEnabled.main+xml`,
|
|
513
|
+
`...ms-word.template.macroEnabledTemplate.main+xml`,
|
|
514
|
+
`...ms-powerpoint.{presentation,slideshow,template,addin}.macroEnabled.main+xml`,
|
|
515
|
+
`...ms-excel.{sheet,template,addin}.macroEnabled.main+xml`,
|
|
516
|
+
`...ms-excel.sheet.binary.macroEnabled.main`.
|
|
517
|
+
5. Exactly one member must have one of these main types, else `FILE_UNSUPPORTED`:
|
|
518
|
+
|
|
519
|
+
| content type (`application/vnd.openxmlformats-officedocument.` +) | format | kind |
|
|
520
|
+
|---|---|---|
|
|
521
|
+
| `wordprocessingml.document.main+xml` | docx | document |
|
|
522
|
+
| `wordprocessingml.template.main+xml` | docx | template |
|
|
523
|
+
| `presentationml.presentation.main+xml`, `presentationml.slideshow.main+xml` | pptx | document |
|
|
524
|
+
| `presentationml.template.main+xml` | pptx | template |
|
|
525
|
+
| `spreadsheetml.sheet.main+xml` | xlsx | document |
|
|
526
|
+
| `spreadsheetml.template.main+xml` | xlsx | template |
|
|
527
|
+
|
|
528
|
+
6. A VBA project anywhere is `FILE_MACRO_ENABLED`: any declared type (`Default`
|
|
529
|
+
or `Override`) equal to `application/vnd.ms-office.vbaProject`, or any member
|
|
530
|
+
whose name ends with `vbaProject.bin` (case-insensitive). A `.docx` may not
|
|
531
|
+
smuggle what a `.docm` declares.
|
|
532
|
+
|
|
533
|
+
**Macro-enabled files are refused, not passed through.** Passing one through
|
|
534
|
+
would hand back, from this system and filled by an agent, a file carrying code
|
|
535
|
+
nothing here inspected; macro documents are the most common malware carrier,
|
|
536
|
+
and last-word's template intake already refuses them. A customer who needs the
|
|
537
|
+
macros keeps them in their own copy. **Encrypted files are refused** because
|
|
538
|
+
there is nothing to fill: the text is ciphertext without the password, and
|
|
539
|
+
"passing it through" would return the input unchanged with every placeholder
|
|
540
|
+
still in it.
|
|
541
|
+
|
|
542
|
+
**The parts that are filled**, by content type (prefix as above):
|
|
543
|
+
|
|
544
|
+
| format | parts |
|
|
545
|
+
|---|---|
|
|
546
|
+
| docx | the main document (`document.main` or `template.main`), `header`, `footer`, `footnotes`, `endnotes`, `comments` (`wordprocessingml.*+xml`) |
|
|
547
|
+
| pptx | `slide`, `slideLayout`, `slideMaster`, `notesSlide`, `notesMaster` (`presentationml.*+xml`) |
|
|
548
|
+
| xlsx | `sharedStrings`, `worksheet` (`spreadsheetml.*+xml`) |
|
|
549
|
+
|
|
550
|
+
**Layouts and masters are filled.** A token on a master or layout (a footer's
|
|
551
|
+
`{{ client }}`, a confidentiality line) is drawn on every slide that uses it,
|
|
552
|
+
so leaving it unfilled would print `{{ client }}` on every slide; that is the
|
|
553
|
+
common reason to put text there at all. A placeholder's prompt text on a layout
|
|
554
|
+
("Click to add title") is never shown in a slide show, so filling a token in it
|
|
555
|
+
is harmless. **Comments are filled** because they ride along with the file and
|
|
556
|
+
an unfilled token in one leaks the template into the customer's review pane.
|
|
557
|
+
|
|
558
|
+
**Every other `.xml` or `.rels` member is still read** (12.7, so a DOCTYPE in
|
|
559
|
+
it is refused) and searched for `{{`: one found is `TOKEN_FORBIDDEN_FIELD` at
|
|
560
|
+
`/<member name>`. A token in a place that is not filled is refused, not left
|
|
561
|
+
in the output, exactly as a profile refuses a field it does not list (section
|
|
562
|
+
9): document properties, charts, SmartArt, a glossary, custom XML.
|
|
563
|
+
|
|
564
|
+
### 12.4 Where text is: containers, runs, text nodes, barriers
|
|
565
|
+
|
|
566
|
+
Elements are matched by namespace URI and local name, never by prefix. Each
|
|
567
|
+
format accepts its transitional and strict namespace:
|
|
568
|
+
|
|
569
|
+
| format | namespace (transitional; strict) |
|
|
570
|
+
|---|---|
|
|
571
|
+
| docx | `http://schemas.openxmlformats.org/wordprocessingml/2006/main`; `http://purl.oclc.org/ooxml/wordprocessingml/main` |
|
|
572
|
+
| pptx | `http://schemas.openxmlformats.org/drawingml/2006/main`; `http://purl.oclc.org/ooxml/drawingml/main` |
|
|
573
|
+
| xlsx | `http://schemas.openxmlformats.org/spreadsheetml/2006/main`; `http://purl.oclc.org/ooxml/spreadsheetml/main` |
|
|
574
|
+
|
|
575
|
+
| format | container | run | text node | transparent run children | barriers |
|
|
576
|
+
|---|---|---|---|---|---|
|
|
577
|
+
| docx | `p` | `r` | `t` child of a run | `rPr`, `delText`, `lastRenderedPageBreak` | every other run child (`tab`, `br`, `cr`, `drawing`, `pict`, `fldChar`, `instrText`, `sym`, `footnoteReference`, ...); `m:oMath`/`m:oMathPara` (OMML namespace) inside the container |
|
|
578
|
+
| pptx | `p` | `r` | `t` child of a run | `rPr` | every other run child; `br` and `fld` inside the container (outside a run) |
|
|
579
|
+
| xlsx | `si`, `is` | `r` | `t` child of a run, or `t` child of the container itself | `rPr` | every other run child |
|
|
580
|
+
|
|
581
|
+
- A **container** is a paragraph (docx/pptx), a shared-string item `si` or an
|
|
582
|
+
inline string `is` (xlsx). A container inside another (a text box's paragraph
|
|
583
|
+
inside a run's drawing) is its own container; the run child holding it
|
|
584
|
+
(`drawing`) is a barrier in the outer one.
|
|
585
|
+
- A **run** is a run element that is a descendant of the innermost open
|
|
586
|
+
container and not inside another run. Containers' other children (`proofErr`,
|
|
587
|
+
`bookmarkStart`, `ins`, `hyperlink`, `fldSimple`, `sdt`, `commentRangeStart`,
|
|
588
|
+
`rPh`, ...) are neither text nor barriers; runs inside them are runs.
|
|
589
|
+
`delText` is not text: deleted text is not shown and is not joined.
|
|
590
|
+
- The text nodes of a container, in document order, split at barriers, form its
|
|
591
|
+
**spans**. A span's **joined text** is its nodes' decoded text (12.7)
|
|
592
|
+
concatenated. An empty text node (`<w:t/>`, `<w:t></w:t>`) is skipped.
|
|
593
|
+
- A container's **location** is `/<member name>#<label>`, the label being the
|
|
594
|
+
container's local name and its 0-based index among the part's containers in
|
|
595
|
+
START-tag order (`#p0`, `#si3`), except an xlsx `is` inside a `c` whose `r`
|
|
596
|
+
attribute is a cell reference (`[A-Z0-9]+`): then the label is that reference
|
|
597
|
+
(`#B4`). A text box's paragraph is numbered after the paragraph holding it.
|
|
598
|
+
- The **visible text** of a container, used by the shared rows, is its text
|
|
599
|
+
nodes' decoded text concatenated (barriers add nothing); in xlsx, a
|
|
600
|
+
`_xHHHH_` escape is then decoded to its character.
|
|
601
|
+
|
|
602
|
+
### 12.5 Tokens, inspection, and the run-split rule
|
|
603
|
+
|
|
604
|
+
Each span whose joined text contains `{{` is scanned with section 5's grammar
|
|
605
|
+
and bounds. A scan failure (`TOKEN_UNCLOSED`, `TOKEN_TOO_LONG`, `TOKEN_EMPTY`,
|
|
606
|
+
`TOKEN_SYNTAX`) is one issue at the container's location, and that span
|
|
607
|
+
contributes no tokens. A token cannot cross a barrier or a container boundary,
|
|
608
|
+
so a `{{` before a tab and its `}}` after it is `TOKEN_UNCLOSED`.
|
|
609
|
+
|
|
610
|
+
Per token, in order: its path (`TOKEN_UNDECLARED` with `variable`, or
|
|
611
|
+
`TOKEN_IMAGE_FIELD`, 12.2), then each filter (`FILTER_UNKNOWN`, `FILTER_ARG`),
|
|
612
|
+
all at the container's location.
|
|
613
|
+
|
|
614
|
+
Issue order: sidecar declarations (12.2), then members in CENTRAL-DIRECTORY
|
|
615
|
+
order; within a filled part, containers in location order, spans in order,
|
|
616
|
+
tokens in order. Placeholders are in the same order. When the file's tokens
|
|
617
|
+
outnumber `limits.tokens`, the issues are exactly `[TEMPLATE_TOO_MANY_TOKENS at ""]`.
|
|
618
|
+
|
|
619
|
+
**The run-split rule.** Word splits text into runs for spell-check marks,
|
|
620
|
+
revision ids and formatting, so `{{ client }}` may be `{{ cli` in one run and
|
|
621
|
+
`ent }}` in the next. A token is found in the span's joined text, then:
|
|
622
|
+
|
|
623
|
+
1. The value is written into the text node where the token STARTS, at the
|
|
624
|
+
token's position, replacing the token's characters in that node. That node's
|
|
625
|
+
run keeps its properties, so **the value takes the formatting of the run
|
|
626
|
+
holding `{{`**: `{{ cli` bold + `ent }}` italic fills bold.
|
|
627
|
+
2. The rest of the token is cut from each following node it covers. Text in
|
|
628
|
+
those nodes outside the token stays, in its own run with its own formatting.
|
|
629
|
+
3. **No node and no run is ever removed**, even one left empty: an empty run is
|
|
630
|
+
invisible, and a run may be what a bookmark, a comment range or a revision
|
|
631
|
+
is anchored to.
|
|
632
|
+
4. Text around a token in a touched node is copied as its ORIGINAL bytes (its
|
|
633
|
+
entities, character references, comments and CDATA as written); only the
|
|
634
|
+
token's own bytes change. A node no token touches is not changed at all.
|
|
635
|
+
|
|
636
|
+
### 12.6 Values
|
|
637
|
+
|
|
638
|
+
A token's value is computed as a `text` field's (sections 6 and 7): resolved
|
|
639
|
+
against the (defaulted) data, filtered, stringified. Then:
|
|
640
|
+
|
|
641
|
+
- It must hold only characters XML 1.0 can carry: a control character other
|
|
642
|
+
than TAB, LF and CR, or U+FFFE / U+FFFF, is `VALUE_XML_CHAR` at the location,
|
|
643
|
+
with `variable`. There is no escape for these in XML 1.0.
|
|
644
|
+
- It is **XML-escaped**: `&` `<` `>` as `&` `<` `>`. Nothing in a
|
|
645
|
+
value can become markup.
|
|
646
|
+
- **xlsx**: a value is written as text in the shared string or inline string
|
|
647
|
+
that held the token, and the cell around it is never touched, so it stays a
|
|
648
|
+
text cell: a value starting with `=`, `+`, `-` or `@` is text, never a
|
|
649
|
+
formula. A `_xHHHH_` sequence in a value has its `_` written as `_x005F_`
|
|
650
|
+
(ECMA-376 ST_Xstring), so Excel shows the value as given instead of decoding it.
|
|
651
|
+
- **A whole-token number in an xlsx cell stays a string.** The token sits in a
|
|
652
|
+
shared string, which may be used by several cells, so turning one cell into a
|
|
653
|
+
number means editing the worksheet's cells and possibly splitting the string;
|
|
654
|
+
the cell's number format was authored for text; and the value has already been
|
|
655
|
+
stringified by section 6's rules, which is what the customer typed the token
|
|
656
|
+
into. A future option may write declared `number` values as number cells;
|
|
657
|
+
until then the rule is: text in, text out.
|
|
658
|
+
- **docx and xlsx white space**: when a touched text node's new text begins or
|
|
659
|
+
ends with white space and its start tag has no `xml:space` attribute,
|
|
660
|
+
` xml:space="preserve"` is inserted before the tag's `>`, because Word and
|
|
661
|
+
Excel otherwise drop it. pptx text keeps its white space and needs none.
|
|
662
|
+
- A value is never re-scanned: `{{` in a value reaches the file literally.
|
|
663
|
+
- `limits.outputBytes` bounds the UTF-8 bytes of all values written
|
|
664
|
+
(`RENDER_TOO_LARGE` at `""`).
|
|
665
|
+
|
|
666
|
+
### 12.7 Reading XML
|
|
667
|
+
|
|
668
|
+
Every member that is read (`[Content_Types].xml`, the filled parts and the
|
|
669
|
+
searched `.xml`/`.rels` members) is refused with `FILE_DOCTYPE` at
|
|
670
|
+
`/<member name>` when it contains `<!DOCTYPE` (ASCII case-insensitive),
|
|
671
|
+
before it is parsed. Nothing defines an entity, so nothing expands one, and
|
|
672
|
+
there is no DTD to fetch.
|
|
673
|
+
|
|
674
|
+
The filled parts and `[Content_Types].xml` are read by a forward-only lexer that
|
|
675
|
+
reports each element with its byte offsets. `FILE_XML` at `/<member name>` for:
|
|
676
|
+
a UTF-16 byte-order mark, an XML declaration naming an encoding other than
|
|
677
|
+
UTF-8, any `<!` other than a comment or CDATA, an unterminated comment, CDATA or
|
|
678
|
+
processing instruction, a mismatched or unclosed tag, more than one root, an
|
|
679
|
+
attribute that is unquoted, repeated or holds `<`, an unbound prefix, an entity
|
|
680
|
+
other than the five predefined ones, a character reference to a character XML
|
|
681
|
+
cannot hold, or an element inside a text node. Text-node content decodes the
|
|
682
|
+
five entities and character references, takes CDATA literally and skips
|
|
683
|
+
comments and processing instructions.
|
|
684
|
+
|
|
685
|
+
A filled part is re-read by the same lexer before it is written; failing that is
|
|
686
|
+
a bug in the implementation, not a file problem.
|
|
687
|
+
|
|
688
|
+
### 12.8 The archive
|
|
689
|
+
|
|
690
|
+
The central directory is parsed and checked before any member is decompressed.
|
|
691
|
+
Bounds, through `options.fileLimits` (the defaults are last-word's):
|
|
692
|
+
|
|
693
|
+
| limit | default | measured as | code |
|
|
694
|
+
|---|---|---|---|
|
|
695
|
+
| `archiveBytes` | 67108864 | the file | `FILE_TOO_LARGE` |
|
|
696
|
+
| `entries` | 16384 | members in the central directory | `FILE_TOO_MANY_ENTRIES` |
|
|
697
|
+
| `partBytes` | 67108864 | any member's declared uncompressed size | `FILE_TOO_LARGE` |
|
|
698
|
+
| `expandedBytes` | 268435456 | all declared uncompressed sizes together | `FILE_TOO_LARGE` |
|
|
699
|
+
| `compressionRatio` | 1000 | a member's uncompressed / max(1, compressed) | `FILE_ZIP_BOMB` |
|
|
700
|
+
|
|
701
|
+
All with path `""`. Also refused, path `""`:
|
|
702
|
+
|
|
703
|
+
| code | when |
|
|
704
|
+
|---|---|
|
|
705
|
+
| `FILE_ARCHIVE` | no end-of-central-directory record that accounts for the whole file; multi-disk; zip64 (an entry count of 65535, a 0xFFFFFFFF size or offset, a zip64 extra field); a method other than stored (0) or deflate (8); a version needed above 2.0; a flag other than bits 1, 2, 3 and 11; a stored member whose sizes differ; a local header that disagrees with the central one (signature, flags, method, name, CRC and sizes unless bit 3 defers them to a matching data descriptor); members that overlap; bytes between the last central record and the end record; a non-ASCII name without the UTF-8 flag; a member whose decompressed length or CRC differs from the directory |
|
|
706
|
+
| `FILE_ENCRYPTED` | the encryption flag (bit 0) or the strong-encryption flag (bit 6), method 99, or an AES extra field (0x9901) |
|
|
707
|
+
| `FILE_PATH` | a name that is empty, starts with `/`, contains `\` or `:` or a control character, is not UTF-8, or has an empty, `.` or `..` segment; two names equal under ASCII case folding (two spellings of one OPC part let a scan read one and an office suite the other) |
|
|
708
|
+
|
|
709
|
+
**Nothing is fetched.** Relationships are never resolved: an external target
|
|
710
|
+
(`TargetMode="External"`, a remote image, a `file://` link) is bytes in a
|
|
711
|
+
`.rels` member, copied with it.
|
|
712
|
+
|
|
713
|
+
**Output.** The output has the input's members in the same local and central
|
|
714
|
+
order with the same archive comment. Every member not filled is its ORIGINAL
|
|
715
|
+
LOCAL RECORD, byte for byte (header, extra field, compressed data, data
|
|
716
|
+
descriptor), and its central record is the original with only the local-header
|
|
717
|
+
offset changed. A filled member is written deflated (or stored, if the
|
|
718
|
+
compressor chooses) with its original timestamp, UTF-8 flag and attributes, no
|
|
719
|
+
extra field, no comment and no data descriptor.
|
|
720
|
+
|
|
721
|
+
### 12.9 Not covered
|
|
722
|
+
|
|
723
|
+
- **Repeating sections** (a table row per line item). They would need: a
|
|
724
|
+
marker that delimits the repeated block in the file's own text (for example
|
|
725
|
+
a row whose first cell holds `{{ #each lines as line }}` and a closing marker),
|
|
726
|
+
the block located at the XML-element level (a `w:tr`, a run of `w:p`, an
|
|
727
|
+
`a:tr`, a worksheet `row`) rather than in text, copies of that element with
|
|
728
|
+
fresh ids where the format demands unique ones (`w14:paraId`, drawing
|
|
729
|
+
`docPr/@id`, bookmark ids, xlsx row numbers and every cell reference, shared
|
|
730
|
+
formula ranges, merged cells and defined names below it), the same
|
|
731
|
+
`iterations` bound, and a rule for what happens to a split marker. That is a
|
|
732
|
+
structural edit, not a text substitution, which is why it is a separate step.
|
|
733
|
+
- Text that is not in a container's text nodes: attribute values (alt text,
|
|
734
|
+
hyperlink targets), field codes (`instrText`), chart and SmartArt text, xlsx
|
|
735
|
+
header/footer strings (`oddHeader`), cell comments, formulas. A token in a
|
|
736
|
+
member that is not filled is refused (12.3); one in a filled member outside
|
|
737
|
+
its text nodes is not seen.
|
|
738
|
+
- A newline in a value is written as a newline character, which Word and
|
|
739
|
+
PowerPoint show as a space; turning it into a break element is not done.
|
|
740
|
+
- ODF (`.odt`, `.odp`, `.ods`), legacy binary formats, `.xlsb`.
|
|
741
|
+
|
|
742
|
+
### 12.10 Shared rows
|
|
743
|
+
|
|
744
|
+
`fixtures/files/cases.json` holds the rows (hand-written from this section)
|
|
745
|
+
and `fixtures/files/*` the files, built by `scripts/files/build-fixtures.php`:
|
|
746
|
+
hand-authored minimal OOXML, plus a docx written by last-word and a pptx written
|
|
747
|
+
by dark-slide (then given a layout, a master and a split run). The rows are in
|
|
748
|
+
fancy-conformance's case-table shape, ready to become its `template/files`
|
|
749
|
+
suite; until then every leg reads them from here. A port is done when it passes
|
|
750
|
+
every row: `expect.text` by visible text (12.4), `expect.changed` as the exact
|
|
751
|
+
set of members whose bytes differ (all others byte-identical records),
|
|
752
|
+
`expect.contains` / `expect.absent` as raw substrings, `expect.error` as the
|
|
753
|
+
thrown error and as `inspectFile`'s single issue.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fancy-template"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Document templates for the Fancy document writers: a writer document with {{ tokens }} and JSON directives, plus a declaration of the data it needs. inspect() at save time, validate_data() before render, render() to plain writer JSON. No code execution, no includes, bounded everywhere. One spec, three runtimes, held to a shared conformance table."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# 3.11, matching every other Fancy Python package (3.10 reaches end of life on
|
|
@@ -28,6 +28,7 @@ from typing import Any
|
|
|
28
28
|
|
|
29
29
|
from ._declarations import apply_defaults, validate_members
|
|
30
30
|
from ._errors import TemplateError
|
|
31
|
+
from ._files import FileTemplatesNotImplementedError, inspect_file, render_file
|
|
31
32
|
from ._inspector import Inspector, declared_variables
|
|
32
33
|
from ._json import first_too_deep, plain
|
|
33
34
|
from ._limits import DEFAULT_LIMITS, Limits, limits_from
|
|
@@ -37,15 +38,18 @@ from ._scanner import tokenize as _scan
|
|
|
37
38
|
|
|
38
39
|
__all__ = [
|
|
39
40
|
"DEFAULT_LIMITS",
|
|
41
|
+
"FileTemplatesNotImplementedError",
|
|
40
42
|
"TemplateError",
|
|
41
43
|
"inspect",
|
|
44
|
+
"inspect_file",
|
|
42
45
|
"preview",
|
|
43
46
|
"render",
|
|
47
|
+
"render_file",
|
|
44
48
|
"tokenize",
|
|
45
49
|
"validate_data",
|
|
46
50
|
]
|
|
47
51
|
|
|
48
|
-
__version__ = "0.
|
|
52
|
+
__version__ = "0.3.0"
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
def inspect(template: Any, options: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Office files as templates (SPEC.md section 12): ``inspect_file`` and
|
|
2
|
+
``render_file``. Implemented in the PHP leg only, so far.
|
|
3
|
+
|
|
4
|
+
They exist in this leg so that a caller gets a LOUD, named refusal rather than
|
|
5
|
+
a missing name or a silent no-op: a ``render_file`` that returned its input
|
|
6
|
+
unchanged would hand back a document full of ``{{ placeholders }}`` and look
|
|
7
|
+
like success. The behaviour to implement is SPEC.md section 12; the shared rows
|
|
8
|
+
and fixtures are in ``fixtures/files/``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Literal, NoReturn
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileTemplatesNotImplementedError(NotImplementedError):
|
|
17
|
+
"""Raised by ``inspect_file`` and ``render_file`` in this language.
|
|
18
|
+
|
|
19
|
+
Office-file templates are implemented in PHP (``Template::inspectFile``),
|
|
20
|
+
not yet here. ``code`` is always ``FILE_NOT_IMPLEMENTED``; ``operation``
|
|
21
|
+
names the function.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
code = "FILE_NOT_IMPLEMENTED"
|
|
25
|
+
|
|
26
|
+
def __init__(self, operation: Literal["inspect_file", "render_file"]) -> None:
|
|
27
|
+
super().__init__(
|
|
28
|
+
f"{operation} is not yet implemented in the Python leg of fancy-template (SPEC.md section 12; PHP has it)."
|
|
29
|
+
)
|
|
30
|
+
self.operation = operation
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def inspect_file(data: bytes, options: dict[str, Any] | None = None) -> NoReturn:
|
|
34
|
+
"""The placeholders in a .docx / .pptx / .xlsx. Not yet implemented here: always raises."""
|
|
35
|
+
raise FileTemplatesNotImplementedError("inspect_file")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_file(data: bytes, values: Any, options: dict[str, Any] | None = None) -> NoReturn:
|
|
39
|
+
"""Fill a .docx / .pptx / .xlsx in place. Not yet implemented here: always raises."""
|
|
40
|
+
raise FileTemplatesNotImplementedError("render_file")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Office-file templates (SPEC.md section 12) are PHP-only so far. This leg must
|
|
2
|
+
refuse LOUDLY and by name -- never return the input, never return nothing."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from fancy_template import FileTemplatesNotImplementedError, TemplateError, inspect_file, render_file
|
|
11
|
+
|
|
12
|
+
DOCX = (Path(__file__).resolve().parents[2] / "fixtures" / "files" / "proposal.docx").read_bytes()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_inspect_file_raises_the_named_error() -> None:
|
|
16
|
+
with pytest.raises(FileTemplatesNotImplementedError) as info:
|
|
17
|
+
inspect_file(DOCX)
|
|
18
|
+
assert info.value.code == "FILE_NOT_IMPLEMENTED"
|
|
19
|
+
assert info.value.operation == "inspect_file"
|
|
20
|
+
assert isinstance(info.value, NotImplementedError)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_render_file_raises_the_named_error_not_a_template_refusal() -> None:
|
|
24
|
+
with pytest.raises(FileTemplatesNotImplementedError) as info:
|
|
25
|
+
render_file(DOCX, {"client": "Ada"})
|
|
26
|
+
assert info.value.operation == "render_file"
|
|
27
|
+
assert not isinstance(info.value, TemplateError)
|
|
28
|
+
assert "SPEC.md section 12" in str(info.value)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|