sphinx-typst-render 0.2.2__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-typst-render
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Render Typst sources into downloadable PDFs and inline previews in Sphinx and Jupyter Book.
5
5
  Keywords: jupyter-book,sphinx,typst,pdf,forms,documentation
6
6
  Author: NB-TUDelft, K.Zabłocki (Zamkorus)
@@ -88,6 +88,7 @@ Pass `:inline:` to also show a preview image and a download link in the body.
88
88
  | `:class:` | none | Extra CSS classes on the wrapper. |
89
89
  | `:inline:` | off | Also show a preview and link in the page body. |
90
90
  | `:fillable:` | off | Add interactive form fields. See below. |
91
+ | `:max-length:` | from config | Characters a text field accepts. `0` strips the limit. |
91
92
 
92
93
  ### Configuration
93
94
 
@@ -98,6 +99,7 @@ Pass `:inline:` to also show a preview image and a download link in the body.
98
99
  | `typst_render_stage_field_library` | `True` | Stage `capture_field.typ` into the source root. |
99
100
  | `typst_render_source_label` | `"Source"` | Heading above the page's own downloads. |
100
101
  | `typst_render_downloads_label` | `"Worksheets"` | Heading above the rendered PDFs. |
102
+ | `typst_render_field_max_length` | `10000` | Characters a text field accepts. `0` strips the limit. |
101
103
 
102
104
  ## The download menu
103
105
 
@@ -178,6 +180,26 @@ yourself.
178
180
  Available helpers are `text_field`, `textarea_field`, `checkbox_field`, and
179
181
  `radio_field`, plus the lower level `capture_field`.
180
182
 
183
+ ## Field length
184
+
185
+ ReportLab caps every text field at 100 characters by default, which stops a
186
+ worksheet answer mid-sentence. This package overrides that with an explicit,
187
+ generous cap:
188
+
189
+ ```yaml
190
+ sphinx:
191
+ config:
192
+ typst_render_field_max_length: 10000 # 0 strips the limit entirely
193
+ ```
194
+
195
+ The value is written explicitly rather than omitted because **some readers
196
+ treat a missing `/MaxLen` as zero** and then refuse all input. Stripping it is
197
+ still available with `0`, for projects that have checked their readers cope.
198
+ Either way it is a config change, not a new release of this package. A single
199
+ block can override it with `:max-length:`.
200
+
201
+ Only text fields are affected. A cap on a checkbox would be meaningless.
202
+
181
203
  ## Caching
182
204
 
183
205
  Output is written beside the source and skipped when nothing relevant changed.
@@ -57,6 +57,7 @@ Pass `:inline:` to also show a preview image and a download link in the body.
57
57
  | `:class:` | none | Extra CSS classes on the wrapper. |
58
58
  | `:inline:` | off | Also show a preview and link in the page body. |
59
59
  | `:fillable:` | off | Add interactive form fields. See below. |
60
+ | `:max-length:` | from config | Characters a text field accepts. `0` strips the limit. |
60
61
 
61
62
  ### Configuration
62
63
 
@@ -67,6 +68,7 @@ Pass `:inline:` to also show a preview image and a download link in the body.
67
68
  | `typst_render_stage_field_library` | `True` | Stage `capture_field.typ` into the source root. |
68
69
  | `typst_render_source_label` | `"Source"` | Heading above the page's own downloads. |
69
70
  | `typst_render_downloads_label` | `"Worksheets"` | Heading above the rendered PDFs. |
71
+ | `typst_render_field_max_length` | `10000` | Characters a text field accepts. `0` strips the limit. |
70
72
 
71
73
  ## The download menu
72
74
 
@@ -147,6 +149,26 @@ yourself.
147
149
  Available helpers are `text_field`, `textarea_field`, `checkbox_field`, and
148
150
  `radio_field`, plus the lower level `capture_field`.
149
151
 
152
+ ## Field length
153
+
154
+ ReportLab caps every text field at 100 characters by default, which stops a
155
+ worksheet answer mid-sentence. This package overrides that with an explicit,
156
+ generous cap:
157
+
158
+ ```yaml
159
+ sphinx:
160
+ config:
161
+ typst_render_field_max_length: 10000 # 0 strips the limit entirely
162
+ ```
163
+
164
+ The value is written explicitly rather than omitted because **some readers
165
+ treat a missing `/MaxLen` as zero** and then refuse all input. Stripping it is
166
+ still available with `0`, for projects that have checked their readers cope.
167
+ Either way it is a config change, not a new release of this package. A single
168
+ block can override it with `:max-length:`.
169
+
170
+ Only text fields are affected. A cap on a checkbox would be meaningless.
171
+
150
172
  ## Caching
151
173
 
152
174
  Output is written beside the source and skipped when nothing relevant changed.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-typst-render"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Render Typst sources into downloadable PDFs and inline previews in Sphinx and Jupyter Book."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-typst-render"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Render Typst sources into downloadable PDFs and inline previews in Sphinx and Jupyter Book."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -5,7 +5,13 @@ from __future__ import annotations
5
5
  import json
6
6
  from pathlib import Path
7
7
 
8
- from ._compile import RenderRequest, RenderResult, render, stage_field_library
8
+ from ._compile import (
9
+ DEFAULT_MAX_LENGTH,
10
+ RenderRequest,
11
+ RenderResult,
12
+ render,
13
+ stage_field_library,
14
+ )
9
15
  from ._directive import TypstDirective
10
16
  from ._downloads import (
11
17
  add_download_buttons,
@@ -15,6 +21,7 @@ from ._downloads import (
15
21
  )
16
22
 
17
23
  __all__ = [
24
+ "DEFAULT_MAX_LENGTH",
18
25
  "RenderRequest",
19
26
  "RenderResult",
20
27
  "TypstDirective",
@@ -22,7 +29,7 @@ __all__ = [
22
29
  "setup",
23
30
  "stage_field_library",
24
31
  ]
25
- __version__ = "0.2.2"
32
+ __version__ = "0.2.4"
26
33
 
27
34
  STATIC_DIR = Path(__file__).parent / "static"
28
35
 
@@ -66,6 +73,9 @@ def setup(app):
66
73
  app.add_config_value("typst_render_preview", "svg", "env")
67
74
  app.add_config_value("typst_render_ppi", 144.0, "env")
68
75
  app.add_config_value("typst_render_stage_field_library", True, "env")
76
+ # Characters a generated text field accepts. Set to 0 to strip the limit
77
+ # entirely, which some readers mis-handle as zero, so it is opt in.
78
+ app.add_config_value("typst_render_field_max_length", DEFAULT_MAX_LENGTH, "env")
69
79
  # Headings inserted into the theme's download menu. The first names what
70
80
  # the page itself is, which differs per project: a manual, a chapter, a
71
81
  # page. An empty string leaves that group unlabelled.
@@ -17,7 +17,12 @@ OUT_DIRNAME = "_typst"
17
17
  LIB_FILENAME = "capture_field.typ"
18
18
 
19
19
  #: Bumped whenever the output layout changes, to invalidate stale caches.
20
- _CACHE_VERSION = 1
20
+ _CACHE_VERSION = 3
21
+
22
+ #: Characters a generated text field accepts unless configured otherwise.
23
+ #: Large enough that no worksheet answer reaches it, and written explicitly
24
+ #: because some readers treat a missing /MaxLen as zero and refuse all input.
25
+ DEFAULT_MAX_LENGTH = 10000
21
26
 
22
27
 
23
28
  @dataclass(frozen=True)
@@ -30,6 +35,9 @@ class RenderRequest:
30
35
  fillable: bool = False
31
36
  ppi: float | None = None
32
37
  preview_page: int = 1
38
+ #: Characters a text field accepts. 0 removes the limit entirely, which
39
+ #: some readers mis-handle as zero. See _apply_field_lengths.
40
+ max_length: int = DEFAULT_MAX_LENGTH
33
41
 
34
42
 
35
43
  @dataclass(frozen=True)
@@ -80,6 +88,7 @@ def _options(request: RenderRequest) -> dict[str, object]:
80
88
  "fillable": request.fillable,
81
89
  "ppi": request.ppi,
82
90
  "page": request.preview_page,
91
+ "max_length": request.max_length,
83
92
  }
84
93
 
85
94
 
@@ -115,6 +124,45 @@ def _fingerprint(request: RenderRequest) -> str:
115
124
  return digest.hexdigest()
116
125
 
117
126
 
127
+ def _apply_field_lengths(writer, max_length: int) -> None:
128
+ """Set, or remove, the character cap on every text field.
129
+
130
+ Three behaviours are in play here. ReportLab's ``AcroForm.textfield()``
131
+ defaults to ``maxlen=100`` and typst-fillable does not override it, so a
132
+ field silently refuses input after 100 characters. Removing ``/MaxLen``
133
+ altogether is not safe either, because some readers treat a missing cap as
134
+ zero and then refuse all input. So the default is to write an explicit,
135
+ generous value, and ``max_length = 0`` drops the key for anyone who has
136
+ checked that their readers cope.
137
+
138
+ Only text fields are touched. A cap on a checkbox would be meaningless.
139
+ """
140
+ from pypdf.generic import NameObject, NumberObject
141
+
142
+ seen: set[int] = set()
143
+
144
+ def apply(obj) -> None:
145
+ if obj is None or id(obj) in seen:
146
+ return
147
+ seen.add(id(obj))
148
+ if obj.get("/FT") == "/Tx":
149
+ if max_length > 0:
150
+ obj[NameObject("/MaxLen")] = NumberObject(max_length)
151
+ elif "/MaxLen" in obj:
152
+ del obj["/MaxLen"]
153
+ for kid in obj.get("/Kids", []) or []:
154
+ apply(kid.get_object())
155
+
156
+ for page in writer.pages:
157
+ for annot in page.get("/Annots", []) or []:
158
+ apply(annot.get_object())
159
+
160
+ acroform = writer._root_object.get("/AcroForm")
161
+ if acroform:
162
+ for field in acroform.get("/Fields", []) or []:
163
+ apply(field.get_object())
164
+
165
+
118
166
  def _add_form_fields(base: bytes, request: RenderRequest) -> bytes:
119
167
  """Overlay interactive AcroForm fields onto an already compiled PDF.
120
168
 
@@ -149,6 +197,7 @@ def _add_form_fields(base: bytes, request: RenderRequest) -> bytes:
149
197
  for index, page in enumerate(reader.pages):
150
198
  if index < len(writer.pages):
151
199
  writer.pages[index].merge_page(page, over=False)
200
+ _apply_field_lengths(writer, request.max_length)
152
201
  # Ask the reader to build field appearances, so a blank field is visible.
153
202
  writer.set_need_appearances_writer(True)
154
203
 
@@ -50,6 +50,7 @@ class TypstDirective(SphinxDirective):
50
50
  "height": directives.length_or_unitless,
51
51
  "ppi": directives.positive_int,
52
52
  "page": directives.positive_int,
53
+ "max-length": directives.nonnegative_int,
53
54
  "class": directives.class_option,
54
55
  }
55
56
 
@@ -79,6 +80,9 @@ class TypstDirective(SphinxDirective):
79
80
  fillable="fillable" in self.options,
80
81
  ppi=self.options.get("ppi", config.typst_render_ppi),
81
82
  preview_page=self.options.get("page", 1),
83
+ max_length=self.options.get(
84
+ "max-length", config.typst_render_field_max_length
85
+ ),
82
86
  )
83
87
 
84
88
  # An inline preview has to be an image Sphinx can see, which means it