metanorma 2.4.3 → 2.5.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -1
- data/lib/metanorma/collection/artifact_store.rb +142 -0
- data/lib/metanorma/collection/filelookup/filelookup.rb +7 -21
- data/lib/metanorma/collection/filelookup/filelookup_sectionsplit.rb +9 -14
- data/lib/metanorma/collection/filelookup/utils.rb +7 -48
- data/lib/metanorma/collection/manifest/manifest.rb +6 -0
- data/lib/metanorma/collection/renderer/fileparse.rb +32 -5
- data/lib/metanorma/collection/renderer/fileprocess.rb +74 -13
- data/lib/metanorma/collection/renderer/renderer.rb +67 -19
- data/lib/metanorma/collection/sectionsplit/sectionsplit.rb +41 -10
- data/lib/metanorma/collection/util/util.rb +16 -0
- data/lib/metanorma/version.rb +1 -1
- data/metanorma.gemspec +1 -1
- data/plans/collection-rendering-architecture-review.md +516 -0
- metadata +7 -9
- data/lib/metanorma/collection/filelookup/base.rb +0 -43
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# Collection rendering — architecture review
|
|
2
|
+
|
|
3
|
+
Tracking issue: metanorma/metanorma#573
|
|
4
|
+
|
|
5
|
+
Internal discussion document for the maintainer (opoudjis). Analysis only; no code
|
|
6
|
+
changes proposed for immediate execution. The overriding constraint throughout is
|
|
7
|
+
**performance must not regress** — this code has had real bottlenecks beaten out of
|
|
8
|
+
it, and several apparently-ugly constructs are load-bearing optimisations. Every
|
|
9
|
+
recommendation below carries an explicit **perf-risk** tag.
|
|
10
|
+
|
|
11
|
+
Scope: `lib/metanorma/collection/` — renderer, filelookup, sectionsplit,
|
|
12
|
+
xrefprocess, manifest, util. File:line references are to the tree as of commit
|
|
13
|
+
`5c05fbd` (the attachment-link sectionsplit fix).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Pipeline map — end-to-end collection render
|
|
18
|
+
|
|
19
|
+
### 1.1 Entry and setup
|
|
20
|
+
|
|
21
|
+
`Collection#render` (`collection.rb:129`) → `Renderer.render` (`renderer.rb:144`).
|
|
22
|
+
`Renderer.render` is the spine:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
cr = new(col, dir, options) # renderer.rb:146 — build FileLookup, isodoc, dirs
|
|
26
|
+
cr.files # renderer.rb:147 — compile every doc (the heavy loop)
|
|
27
|
+
cr.rxl(options) # renderer.rb:148 — write collection.rxl
|
|
28
|
+
cr.concatenate(col, options) # renderer.rb:149 — build collection.xml / .presentation.xml / pdf / doc
|
|
29
|
+
cr.coverpage # renderer.rb:150 — Liquid coverpage (html only)
|
|
30
|
+
cr.flush_files # renderer.rb:151 — delete temp + sectionsplit leftovers
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`Renderer#initialize` (`renderer.rb:36-85`) is where the *file table* is built:
|
|
34
|
+
|
|
35
|
+
- `@xml = Nokogiri::XML collection.to_xml` (`renderer.rb:38`) — the collection
|
|
36
|
+
manifest, parsed once and kept.
|
|
37
|
+
- `@files = FileLookup.new(folder, self)` (`renderer.rb:81`) — reads **every**
|
|
38
|
+
source XML, extracts bibdata/anchors/ids per file (see §1.5).
|
|
39
|
+
- `@files.add_section_split` (`renderer.rb:82`) — **expands sectionsplit documents
|
|
40
|
+
in place** before the main render loop ever runs (see §2). This is the single most
|
|
41
|
+
surprising piece of control flow: by the time `cr.files` runs, a sectionsplit
|
|
42
|
+
document has already been split into N sub-entries in `@files`, each compiled to
|
|
43
|
+
presentation XML on disk.
|
|
44
|
+
|
|
45
|
+
### 1.2 The per-file loop (`fileprocess.rb:59` `#files`)
|
|
46
|
+
|
|
47
|
+
For each identifier in `@files.keys`:
|
|
48
|
+
|
|
49
|
+
- **attachment** → `copy_file_to_dest` (`filelocation.rb:149`), no compile.
|
|
50
|
+
- **document** → read source (Semantic XML), `update_xrefs` (`fileparse.rb:17`),
|
|
51
|
+
write the resolved XML to a tmp file, then `file_compile` (`fileprocess.rb:13`).
|
|
52
|
+
|
|
53
|
+
`internal_refs = locate_internal_refs` (`fileprocess.rb:61`) is computed **once**
|
|
54
|
+
before the loop and threaded through every `update_xrefs` call — a deliberate
|
|
55
|
+
hoist (see §1.5).
|
|
56
|
+
|
|
57
|
+
### 1.3 Semantic vs Presentation XML at each stage
|
|
58
|
+
|
|
59
|
+
This is the axis that makes the code hard to read, because the same variable names
|
|
60
|
+
(`xml`, `file`, `docxml`) carry *different* document grammars at different points:
|
|
61
|
+
|
|
62
|
+
| Stage | Grammar | Where |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Source files read by FileLookup | **Semantic** | `filelookup.rb:81`, `:ref` files |
|
|
65
|
+
| `update_xrefs` input for a normal doc | **Semantic** | `fileprocess.rb:69`, `fileparse.rb:17` |
|
|
66
|
+
| `update_xrefs` input for a sectionsplit-output sub-file | **Presentation** (`:sectionsplit_output` truthy) | `fileparse.rb:34-39` |
|
|
67
|
+
| Sectionsplit `sectionsplit_prep` output | **Presentation** | `sectionsplit.rb:98-109` (compiles to `.presentation.xml`) |
|
|
68
|
+
| `file_compile` → flavour `Compile#compile` | Semantic → all formats | `fileprocess.rb:19` |
|
|
69
|
+
| `concatenate1` collecting per-doc outputs | Presentation/whatever ext | `renderer.rb:256-267` |
|
|
70
|
+
|
|
71
|
+
The pivot is `sso = @files.get(docid, :sectionsplit_output)` (`fileparse.rb:38`).
|
|
72
|
+
**`sso` truthy ⇒ the file is already Presentation XML**, so `update_xrefs` skips the
|
|
73
|
+
semantic-only passes (`xref_process`, indirect/sectionsplit doc resolution,
|
|
74
|
+
svgmap) and only does direct-ref + hide_refs + eref2link. This is correct but
|
|
75
|
+
**entirely implicit** — the only documentation is the one-line comment at
|
|
76
|
+
`fileparse.rb:34`.
|
|
77
|
+
|
|
78
|
+
### 1.4 The three reference-resolution passes (`fileparse.rb:17-32` `#update_xrefs`)
|
|
79
|
+
|
|
80
|
+
The header comment (`fileparse.rb:4-10`) is the best existing summary. In order:
|
|
81
|
+
|
|
82
|
+
1. **`xref_process`** (`fileparse.rb:18-21`) — only when `!@nested && !sso`.
|
|
83
|
+
Delegates to `XrefProcess.xref_process` (`xrefprocess.rb:26`): turns intra-doc
|
|
84
|
+
`xref`/`eref` into internal erefs, copies repo bibitems, inserts indirect biblio.
|
|
85
|
+
2. **`update_indirect_refs_to_docs`** (`fileparse.rb:23`, body `fileparse.rb:133`)
|
|
86
|
+
— resolves `bibitem[@type='internal']` repository refs (anchor in an unknown
|
|
87
|
+
collection file) to a concrete containing document.
|
|
88
|
+
3. **`add_document_suffix`** (`fileparse.rb:24` → `filelookup.rb:237`) — namespaces
|
|
89
|
+
every `@id`/`@anchor` with a per-doc NCName suffix so concatenated docs don't
|
|
90
|
+
collide. Mutates the tree.
|
|
91
|
+
4. **`update_sectionsplit_refs_to_docs`** (`fileparse.rb:25`, body `fileparse.rb:42`)
|
|
92
|
+
— rewrites erefs that target a *sectionsplit* document to point at the specific
|
|
93
|
+
split section file that contains the anchor.
|
|
94
|
+
5. **`update_direct_refs_to_docs`** (`fileparse.rb:27`, body `fileparse.rb:88`) —
|
|
95
|
+
`repo(current-metanorma-collection/X)` → hyperlink + bibdata in situ; calls the
|
|
96
|
+
`update_anchors` **bottleneck** (§3.6).
|
|
97
|
+
6. **`hide_refs`** (`fileparse.rb:28` → `util.rb:71`) — flags now-empty hidden
|
|
98
|
+
references containers.
|
|
99
|
+
7. `eref2link` / `svgmap_resolve` post-passes.
|
|
100
|
+
|
|
101
|
+
All of 1-7 mutate the *same* Nokogiri tree in place. There is no intermediate
|
|
102
|
+
typed representation; the contract between passes is "the tree is now in state X",
|
|
103
|
+
documented only by reading the passes in order.
|
|
104
|
+
|
|
105
|
+
### 1.5 The lookup tables (built once, read hot)
|
|
106
|
+
|
|
107
|
+
`locate_internal_refs` (`fileprocess.rb:132`) builds a `schema → anchor → filename`
|
|
108
|
+
map by:
|
|
109
|
+
|
|
110
|
+
- `gather_internal_refs` (`fileprocess.rb:87`) — re-parses every non-attachment,
|
|
111
|
+
non-sectionsplit source file (`Nokogiri::XML(file, &:huge)` at `:97`) to collect
|
|
112
|
+
indirect-ref targets;
|
|
113
|
+
- `populate_internal_refs` (`fileprocess.rb:121`) — re-parses **every file again**
|
|
114
|
+
(`locate_internal_refs1` → `locate_internal_refs1_prep`, `fileprocess.rb:156`) to
|
|
115
|
+
build an `id/anchor → element` index per file and match the wanted ids.
|
|
116
|
+
|
|
117
|
+
So the reference graph costs **two full re-parses of every source document** before
|
|
118
|
+
the main loop. This is a known cost centre (see §3.1) but the result is hoisted out
|
|
119
|
+
of the per-file loop (`fileprocess.rb:61`), so it is paid once, not O(files²).
|
|
120
|
+
|
|
121
|
+
### 1.6 Concatenate / coverage
|
|
122
|
+
|
|
123
|
+
`concatenate` (`renderer.rb:164`) builds `collection.xml` and, if pdf/doc/bilingual
|
|
124
|
+
is requested, `collection.presentation.xml`, then runs mn2pdf / doc / bilingual
|
|
125
|
+
HTML. `concatenate1` (`renderer.rb:256`) pulls each doc's already-compiled output
|
|
126
|
+
back off disk via `@files.get(id, :outputs)[ext]`. Coverpage is a Liquid template
|
|
127
|
+
fill (`renderer.rb:277`).
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 2. Sectionsplit deep-dive
|
|
132
|
+
|
|
133
|
+
Sectionsplit is hard because it is **a collection render nested inside a collection
|
|
134
|
+
render**, wired together through the file table and several temp directories. Two
|
|
135
|
+
distinct entry paths exist.
|
|
136
|
+
|
|
137
|
+
### 2.1 Path A — sectionsplit inside an existing collection (the FileLookup path)
|
|
138
|
+
|
|
139
|
+
Triggered from `FileLookup#add_section_split` (`filelookup_sectionsplit.rb:6`),
|
|
140
|
+
called at `renderer.rb:82` during renderer construction, **before** the main loop.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
add_section_split filelookup_sectionsplit.rb:6
|
|
144
|
+
for each @files entry with :sectionsplit and not :attachment
|
|
145
|
+
process_section_split_instance filelookup_sectionsplit.rb:17
|
|
146
|
+
original_out_path = @files[key][:out_path] # saved for cleanup
|
|
147
|
+
s, manifest = sectionsplit(key) filelookup_sectionsplit.rb:142
|
|
148
|
+
Sectionsplit.new(...).sectionsplit sectionsplit.rb:41
|
|
149
|
+
Sectionsplit#collection_manifest sectionsplit/collection.rb:34
|
|
150
|
+
for each split section file f1:
|
|
151
|
+
add_section_split_instance filelookup_sectionsplit.rb:98
|
|
152
|
+
-> inserts a NEW @files entry per section (presentation XML,
|
|
153
|
+
:sectionsplit_output=true, :parentid=key, :bare for idx>0)
|
|
154
|
+
add_section_split_attachments filelookup_sectionsplit.rb:89
|
|
155
|
+
add_section_split_cover filelookup_sectionsplit.rb:59
|
|
156
|
+
cleanup_section_split_instance filelookup_sectionsplit.rb:44
|
|
157
|
+
schedule original parent html/xml/presentation.xml for deletion
|
|
158
|
+
@files[key][:indirect_key] = @sectionsplit.key
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The parent document's `@files` entry survives but is turned into a cover/index
|
|
162
|
+
attachment-like entry (`add_section_split_cover`, `filelookup_sectionsplit.rb:59`
|
|
163
|
+
sets `:out_path = cover`), and N new entries — one per section — are inserted.
|
|
164
|
+
**The `@files` hash is mutated and re-built** (`add_section_split` rebuilds it into
|
|
165
|
+
`ret` at `:7-14`).
|
|
166
|
+
|
|
167
|
+
### 2.2 The split itself (`sectionsplit.rb`)
|
|
168
|
+
|
|
169
|
+
`Sectionsplit#sectionsplit` (`sectionsplit.rb:41`):
|
|
170
|
+
|
|
171
|
+
1. `sectionsplit_prep` (`:98`) — reads the **Semantic** source, runs
|
|
172
|
+
`sectionsplit_update_xrefs` (`:121`, which re-enters the *parent's*
|
|
173
|
+
`update_xrefs` with `@nested=true` so unresolved erefs survive), writes a temp
|
|
174
|
+
semantic file, then **compiles it to Presentation XML** via a fresh
|
|
175
|
+
`Compile.new.compile` (`:104`) and reloads the `.presentation.xml`. So from here
|
|
176
|
+
on `xml` is Presentation XML.
|
|
177
|
+
2. `xref_preprocess` (`:43` → `xrefprocess.rb:9`) — stamps a random 8-char `key` on
|
|
178
|
+
the root and suffixes all anchor attrs. The `key` becomes the document's
|
|
179
|
+
`:indirect_key` and ties the split files back together.
|
|
180
|
+
3. `empty_doc` (`:140`) — clones the doc and strips all section content to make the
|
|
181
|
+
**template** every section file is built from. `empty_attachments` (`:151`) is
|
|
182
|
+
just `xml.dup` (a second template used for idx>0; the only difference is the
|
|
183
|
+
first file keeps the section-free `empty`).
|
|
184
|
+
4. `sectionsplit1` (`:53`) walks `SPLITSECTIONS` (`:34-38`: preface, sections,
|
|
185
|
+
annex, bibliography, indexsect, colophon), conflates floating titles
|
|
186
|
+
(`conflate_floatingtitles`, `:88`), and for each chunk posts a job to a
|
|
187
|
+
**thread pool of size 1** (`:48`) that calls `sectionfile` (`:155`).
|
|
188
|
+
5. `sectionfile` → `create_sectionfile` (`:161`): inserts the chunk into a clone of
|
|
189
|
+
the template, filters footnotes/annotations down to those referenced
|
|
190
|
+
(`sectionfile_fn_filter` `:185`, `sectionfile_annotation_filter` `:237`), runs
|
|
191
|
+
`XrefProcess.xref_process(out, xml, @key, ...)` to resolve cross-refs within the
|
|
192
|
+
section, and writes the section file.
|
|
193
|
+
|
|
194
|
+
### 2.3 The flat-XML-vs-directory-HTML output split
|
|
195
|
+
|
|
196
|
+
This is the subtle invariant that the recent bug (`5c05fbd`) lives next to:
|
|
197
|
+
|
|
198
|
+
- **XML section files are always written flat** to `@splitdir` (the `_files`
|
|
199
|
+
directory), basename only — `create_sectionfile` (`sectionsplit.rb:166-174`) is
|
|
200
|
+
explicit about this in comments.
|
|
201
|
+
- **HTML output may carry a directory** from `sectionsplit_filename` (e.g.
|
|
202
|
+
`split/{basename}.html`). The directory is reattached only at HTML compile time
|
|
203
|
+
via `preserve_directory_structure?` (`filelookup.rb:359`) and the
|
|
204
|
+
`file_compile_format` machinery (`filelocation.rb:65-79`).
|
|
205
|
+
|
|
206
|
+
The manifest YAML (`collectionyaml`, `sectionsplit/collection.rb:41-73`) encodes
|
|
207
|
+
this: `fileref` is always the basename (`:60`), but
|
|
208
|
+
`sectionsplit-filename`/`sectionsplit-output` are emitted only when there is a
|
|
209
|
+
directory (`:64-67`) so the *inner* renderer knows to re-expand it.
|
|
210
|
+
|
|
211
|
+
### 2.4 Path B — single-file sectionsplit building its own collection
|
|
212
|
+
|
|
213
|
+
`Sectionsplit#build_collection` (`sectionsplit/collection.rb:4`): used when a single
|
|
214
|
+
document is sectionsplit on its own. It runs `sectionsplit`, writes a generated
|
|
215
|
+
`*.html.yaml` manifest (`collectionyaml`), and calls `Metanorma::Collection.parse`
|
|
216
|
+
+ `.render` recursively (`:9-12`) — a **fully nested collection render** — then moves
|
|
217
|
+
attachments (`section_split_attachments`, `:79`).
|
|
218
|
+
|
|
219
|
+
### 2.5 Attachment handling and the early/late asymmetry (the recent bug)
|
|
220
|
+
|
|
221
|
+
`section_split_attachments` (`sectionsplit/collection.rb:79`) moves the
|
|
222
|
+
`_<basename>_attachments` directory from the temp split location to the collection
|
|
223
|
+
output. The attachment *links* inside section files are resolved during
|
|
224
|
+
`update_bibitem` (`fileparse.rb:170`).
|
|
225
|
+
|
|
226
|
+
The bug fixed in `5c05fbd`: attachment URLs were relativised against the
|
|
227
|
+
referencing document's **unsplit `:out_path`**, but a sectionsplit document's
|
|
228
|
+
content is actually emitted at the split output location, so `../../` overshot. The
|
|
229
|
+
fix introduced `referencing_html_location` (`fileparse.rb:208`) + helpers
|
|
230
|
+
`sectionsplit_ref_html` / `document_ref_html` (`:216`, `:223`) to choose the right
|
|
231
|
+
base. The regression spec (`spec/collection/attachment_link_path_spec.rb`) pins all
|
|
232
|
+
four cases. This fix is correct, but it is a *symptom* of pain point §3.2.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 3. Maintainability pain points (evidence-based)
|
|
237
|
+
|
|
238
|
+
### 3.1 The reference graph is built from repeated full re-parses
|
|
239
|
+
`gather_internal_refs1` (`fileprocess.rb:97`), `locate_internal_refs1_prep`
|
|
240
|
+
(`fileprocess.rb:156`), and FileLookup's own `bibdata_process` (`filelookup.rb:81`)
|
|
241
|
+
each `Nokogiri::XML(file, &:huge)` the *same* source files. A given source document
|
|
242
|
+
is parsed at least 3× before its content is even compiled. **This is load-bearing
|
|
243
|
+
optimisation territory, not just waste** — see perf note in §4.
|
|
244
|
+
|
|
245
|
+
### 3.2 Early-vs-late reference resolution asymmetry
|
|
246
|
+
Some links are resolved *before* the split, against the unsplit `out_path`
|
|
247
|
+
(`update_bibitem` path); others are resolved *during* the inner render of the split
|
|
248
|
+
files. The two views of "where does this document's content live" diverge under
|
|
249
|
+
sectionsplit, which is exactly what produced the `5c05fbd` bug. The new
|
|
250
|
+
`referencing_html_location` (`fileparse.rb:208`) papers over one instance, but the
|
|
251
|
+
underlying asymmetry — *out_path is not where the content ends up for a sectionsplit
|
|
252
|
+
doc* — is undocumented as an invariant and will bite again (e.g. svgmap, images,
|
|
253
|
+
PDF cross-links).
|
|
254
|
+
|
|
255
|
+
### 3.3 Path computation is scattered across ≥4 files
|
|
256
|
+
Relative/output path logic lives in: `filelocation.rb`
|
|
257
|
+
(`preserve_output_dir_structure`, `move_file_to_subdirectory`,
|
|
258
|
+
`apply_custom_filename_pattern`, `:39-147`), `fileparse.rb`
|
|
259
|
+
(`referencing_html_location`, `sectionsplit_ref_html`, `document_ref_html`,
|
|
260
|
+
`out_path_to_html`, `:208-232`), `utils.rb` (`make_relative_path`, `:91`),
|
|
261
|
+
`filelookup.rb` (`output_file_path`, `file_entry_paths`,
|
|
262
|
+
`substitute_filename_pattern`, `ref_file_xml2html`, `:145-312`), and
|
|
263
|
+
`filelookup_sectionsplit.rb` (`add_section_split_instance` path assembly,
|
|
264
|
+
`:98-130`). Each does its own `File.dirname`/`File.basename`/`relative_path_from`
|
|
265
|
+
dance with slightly different rules (when to strip `.xml`, when to keep dirs, when
|
|
266
|
+
to disambiguate). The placeholder substitution (`{basename}`, `{basename_legacy}`,
|
|
267
|
+
`{document-num}`, `{sectionsplit-num}`) is implemented **twice** — once in
|
|
268
|
+
`FileLookup#substitute_filename_pattern` (`filelookup.rb:168`) and once inline in
|
|
269
|
+
`Sectionsplit#sectionsplit2` (`sectionsplit.rb:69-73`).
|
|
270
|
+
|
|
271
|
+
### 3.4 The `@files` entry is an untyped grab-bag
|
|
272
|
+
A FileLookup entry is a bare `Hash` carrying ~26 distinct keys (counted across the
|
|
273
|
+
codebase): `:format :outputs :out_path :url :type :attachment :ref :bibdata
|
|
274
|
+
:bibitem :sectionsplit :sectionsplit_output :sectionsplit_filename :rel_path
|
|
275
|
+
:indirect_key :index :parentid :idx :ids :document_suffix :presentationxml :pdffile
|
|
276
|
+
:bare :anchors :anchors_lookup :output_filename :extract_opts`. Access is via
|
|
277
|
+
`@files.get(id, :key)` (`base.rb:20`) with no schema, no validation, and several
|
|
278
|
+
near-synonyms whose distinction is only learnable by reading every writer:
|
|
279
|
+
- `:ref` (absolute source) vs `:rel_path` (relative to YAML) vs `:out_path`
|
|
280
|
+
(destination) vs `:url` vs `:outputs[:html]` (post-compile actual path) — five
|
|
281
|
+
notions of "where is this file", documented only in a comment block at
|
|
282
|
+
`filelookup.rb:102-107` and `:128-134`.
|
|
283
|
+
- `:idx` vs `:index`; `:sectionsplit` vs `:sectionsplit_output`;
|
|
284
|
+
`:sectionsplit_filename` vs `:output_filename`.
|
|
285
|
+
- `:type` holds the string `"fileref"`/`"id"` (`filelookup.rb:116,122`) — unrelated
|
|
286
|
+
to document flavour `type`.
|
|
287
|
+
|
|
288
|
+
There is no single place that says "these are the fields and what they mean."
|
|
289
|
+
|
|
290
|
+
### 3.5 Duplicated / divergent method definitions in FileLookup
|
|
291
|
+
`filelookup/utils.rb` and `filelookup/base.rb` and `filelookup/filelookup.rb`
|
|
292
|
+
**redefine the same methods**. `filelookup.rb` requires both `base` (`:6`) and
|
|
293
|
+
`utils` (`:7`), with `utils` last. Duplicated: `read_ids`, `read_anchors`,
|
|
294
|
+
`read_anchors1`, `anchors_lookup`, `url`, `url?`, `key`, `keys`, `get`, `set`,
|
|
295
|
+
`each`, `each_with_index`, `ns`. Because `utils.rb` is required last, **its
|
|
296
|
+
definitions win** — and `read_anchors1` in `utils.rb` (`utils.rb:29-38`) uses the
|
|
297
|
+
regex `%r{<[^<>]+>}` whereas the copy in `filelookup.rb` (`:335-344`) uses
|
|
298
|
+
`%r{<[^>]+>}`. Whichever is actually live is decided by require order, not by
|
|
299
|
+
intent. This is a latent correctness hazard, not merely cosmetic. (`utils.rb` looks
|
|
300
|
+
like an extraction-in-progress that was never completed or wired exclusively.)
|
|
301
|
+
|
|
302
|
+
### 3.6 The `update_anchors` bottleneck (`fileparse.rb:195`, marked `# bottleneck`)
|
|
303
|
+
For each repository bibitem, `update_anchors` iterates `erefs_no_anchor` and
|
|
304
|
+
`erefs_anchors` and calls `update_anchors1` (`:208`) which consults
|
|
305
|
+
`@files.get(docid).dig(:anchors_lookup, ...)`. The `:anchors_lookup` table
|
|
306
|
+
(`anchors_lookup`, `utils.rb:40` / `filelookup.rb` via `bibdata_extract`) is a
|
|
307
|
+
flattened `{anchor => true}` set precomputed per file — this is the optimisation
|
|
308
|
+
that keeps the lookup O(1) instead of re-xpathing the target doc per eref. **Do not
|
|
309
|
+
remove `:anchors_lookup`** (see §5).
|
|
310
|
+
|
|
311
|
+
### 3.7 `@nested` flag overloading
|
|
312
|
+
`@nested` (`renderer.rb:70`) means "this Renderer will run again, don't do
|
|
313
|
+
finalising ref work." It gates five different behaviours in `update_xrefs`
|
|
314
|
+
(`fileparse.rb:19,23,25,30`) and the strip-unresolved logic
|
|
315
|
+
(`fileparse.rb:126`). Sectionsplit toggles it on the *parent* renderer temporarily
|
|
316
|
+
(`sectionsplit.rb:122-126`: save, set true, call, restore). One boolean encoding
|
|
317
|
+
"am I the root render?" + "should I preserve unresolved erefs?" + "skip svgmap" is
|
|
318
|
+
hard to reason about; the save/restore-around-call idiom is a smell that it is
|
|
319
|
+
really a per-call parameter, not object state.
|
|
320
|
+
|
|
321
|
+
### 3.8 Nested-manifest in-place expansion
|
|
322
|
+
`Manifest#manifest_expand_yaml` (`manifest.rb:118`) mutates entries in place,
|
|
323
|
+
setting `entry.entry = ...from_yaml(...)` and rewriting filepaths via
|
|
324
|
+
`update_filepaths` (`:141`). Combined with the several `manifest_*` passes that each
|
|
325
|
+
recurse the tree independently (`manifest_postprocess`, `:23-32` runs 7 separate
|
|
326
|
+
recursive walks), the manifest is normalised by a pipeline of mutating tree-walks
|
|
327
|
+
with order dependencies that are not stated.
|
|
328
|
+
|
|
329
|
+
### 3.9 Identifier/key normalisation duplicated
|
|
330
|
+
`key` is defined in `Util::key` (`util.rb:79`), `FileLookup#key` (`base.rb:11`,
|
|
331
|
+
`utils.rb:61`), and the decode+squeeze pattern recurs in `docid_prefix`
|
|
332
|
+
(`utils.rb` renderer, `:105`). `FileLookup#key` additionally strips a
|
|
333
|
+
`metanorma-collection ` prefix (`base.rb:13`) that `Util::key` does not — so they
|
|
334
|
+
are *not* interchangeable, yet both are called "key". Mixing them is a footgun.
|
|
335
|
+
|
|
336
|
+
### 3.10 Mutation-in-place through many passes
|
|
337
|
+
Every resolution pass mutates the shared Nokogiri tree (§1.4). There is no
|
|
338
|
+
checkpoint, no copy, no assertion of post-conditions. Debugging "which pass put the
|
|
339
|
+
tree in this state" means instrumenting each pass. This is partly inherent to
|
|
340
|
+
Nokogiri performance (copying trees is expensive), but the *absence of documented
|
|
341
|
+
invariants between passes* is the maintainability cost, not the mutation itself.
|
|
342
|
+
|
|
343
|
+
### 3.11 Dead / commented-out code and `warn` debug noise
|
|
344
|
+
Numerous `# KILL`, commented thread-pool variants, and `warn` timing/debug lines
|
|
345
|
+
remain: `renderer.rb:93-106` (two methods named `directives_normalise_coverpage_pdf_portfolio`,
|
|
346
|
+
the **second silently shadows the first**), `renderer.rb:132,145,165,179,200` warn
|
|
347
|
+
spam, `filelookup.rb:212` `warn ret`, `filelookup_sectionsplit.rb:32-42`
|
|
348
|
+
(`section_split_instance_threads` defined but unused), `fileprocess.rb:48-55`
|
|
349
|
+
commented `allowed_extension_keys`. The duplicate `directives_normalise_coverpage_pdf_portfolio`
|
|
350
|
+
(`renderer.rb:94` and `:108`) is an actual bug-shaped artifact: Ruby keeps the
|
|
351
|
+
second definition, so the first (more elaborate) one is dead.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 4. Prioritised recommendations
|
|
356
|
+
|
|
357
|
+
Each carries a **perf-risk** tag: `NEUTRAL` (no runtime change), `COULD-HELP`,
|
|
358
|
+
`RISK` (could reintroduce a bottleneck — flagged loudly), `BENIGN-CLEANUP`.
|
|
359
|
+
|
|
360
|
+
### HIGH
|
|
361
|
+
|
|
362
|
+
**H1. Document the file-grammar and out_path invariants as code comments + a
|
|
363
|
+
one-page note.** State explicitly: (a) when `xml`/`file` is Semantic vs Presentation
|
|
364
|
+
and that `:sectionsplit_output` is the discriminator; (b) the load-bearing invariant
|
|
365
|
+
"for a sectionsplit document, content is emitted at the sectionsplit output
|
|
366
|
+
location, NOT at `:out_path`" (the §3.2 root cause). Put it at the top of
|
|
367
|
+
`fileparse.rb` and `sectionsplit.rb`.
|
|
368
|
+
*Gain:* directly prevents recurrences of the `5c05fbd` class of bug.
|
|
369
|
+
**perf-risk: NEUTRAL** (comments only).
|
|
370
|
+
|
|
371
|
+
**H2. Resolve the FileLookup method duplication (§3.5).** Pick one home for
|
|
372
|
+
`read_ids`/`read_anchors`/`read_anchors1`/`anchors_lookup`/`url`/`key`/etc., delete
|
|
373
|
+
the others, and reconcile the divergent `read_anchors1` regex (`<[^<>]+>` vs
|
|
374
|
+
`<[^>]+>`) deliberately. Right now correctness depends on require order.
|
|
375
|
+
*Gain:* removes a latent correctness hazard and ~80 lines of confusing duplication.
|
|
376
|
+
**perf-risk: NEUTRAL** (same code runs; you are just deleting the shadowed copy).
|
|
377
|
+
*Caveat:* verify which regex is currently live before deleting, so behaviour is
|
|
378
|
+
preserved exactly unless you intend to change it.
|
|
379
|
+
|
|
380
|
+
**H3. Introduce a typed `FileEntry` value object wrapping the `@files` hash
|
|
381
|
+
(§3.4).** Keep the hash as the backing store for perf, but give it named readers and
|
|
382
|
+
a documented field list, plus predicate methods (`#sectionsplit?`,
|
|
383
|
+
`#sectionsplit_output?`, `#attachment?`). Do this incrementally behind the existing
|
|
384
|
+
`get/set` API so call sites can migrate gradually.
|
|
385
|
+
*Gain:* the single biggest readability win; makes the 26-key grab-bag legible.
|
|
386
|
+
**perf-risk: COULD-HELP if done as a thin wrapper, RISK if naively done.** A
|
|
387
|
+
`Struct`/`Data` rebuilt per access, or replacing the hash with per-call object
|
|
388
|
+
allocation in the hot `update_anchors`/`gather_*` loops, **would** add allocation
|
|
389
|
+
pressure. Mark clearly: the wrapper must be allocated **once per entry**, not per
|
|
390
|
+
access, and the hot paths (`fileparse.rb:196-237`, `util.rb:17-45`) should keep
|
|
391
|
+
using direct hash reads or memoised readers. Stage it: wrapper first for the
|
|
392
|
+
cold-path call sites (manifest, setup), leave the bottleneck loops on raw hash
|
|
393
|
+
access until measured.
|
|
394
|
+
|
|
395
|
+
### MEDIUM
|
|
396
|
+
|
|
397
|
+
**M1. Consolidate path computation into one module (§3.3).** Gather
|
|
398
|
+
`make_relative_path`, `out_path_to_html`, `ref_file_xml2html`, `output_file_path`,
|
|
399
|
+
`preserve_output_dir_structure`, `substitute_filename_pattern`, and the inline
|
|
400
|
+
substitution in `sectionsplit.rb:69-73` into a single `PathResolver` with
|
|
401
|
+
well-named, individually-tested methods. De-duplicate the two placeholder
|
|
402
|
+
substituters first (they should be one function).
|
|
403
|
+
*Gain:* the next path bug (and §3.2 predicts more) gets fixed in one place with a
|
|
404
|
+
test, instead of hunting four files.
|
|
405
|
+
**perf-risk: NEUTRAL.** Pure string/Pathname math, not in an XML-parsing hot loop;
|
|
406
|
+
moving it does not change call frequency. Keep the functions pure (no I/O) so they
|
|
407
|
+
stay cheap.
|
|
408
|
+
|
|
409
|
+
**M2. Turn `@nested` into an explicit parameter or a small mode object (§3.7).**
|
|
410
|
+
The save/set-true/call/restore dance in `sectionsplit_update_xrefs`
|
|
411
|
+
(`sectionsplit.rb:122-126`) is the tell. Pass a `render_mode:` (`:root` /
|
|
412
|
+
`:nested`) argument through `update_xrefs`, or split `update_xrefs` into
|
|
413
|
+
`update_xrefs_root` / `update_xrefs_nested` that share helpers.
|
|
414
|
+
*Gain:* removes hidden global-ish state; makes the sectionsplit re-entry legible.
|
|
415
|
+
**perf-risk: NEUTRAL** (same branches, just parameterised).
|
|
416
|
+
|
|
417
|
+
**M3. Name the sectionsplit control flow seams (§2).** Extract well-named methods
|
|
418
|
+
for the three conceptual phases that are currently interleaved in
|
|
419
|
+
`add_section_split` / `process_section_split_instance`: *split-into-section-files*,
|
|
420
|
+
*register-section-entries-in-@files*, *register-cover-and-attachments*. The logic
|
|
421
|
+
need not move; just give the phases names and a comment each.
|
|
422
|
+
*Gain:* the hardest-to-follow file becomes a readable three-step.
|
|
423
|
+
**perf-risk: NEUTRAL** (method extraction only).
|
|
424
|
+
|
|
425
|
+
**M4. Delete dead code and shadowed definitions (§3.11).** Remove the shadowed
|
|
426
|
+
`directives_normalise_coverpage_pdf_portfolio` (`renderer.rb:94` — confirm which is
|
|
427
|
+
intended), unused `section_split_instance_threads`, commented `allowed_extension_keys`,
|
|
428
|
+
`# KILL` blocks. Gate the `warn` timing lines behind a debug flag rather than
|
|
429
|
+
unconditional stderr spam.
|
|
430
|
+
*Gain:* less noise, removes a real shadowing bug.
|
|
431
|
+
**perf-risk: BENIGN-CLEANUP** (removing `warn` calls is a micro-improvement; the
|
|
432
|
+
shadowed-method removal must preserve whichever definition is actually wanted).
|
|
433
|
+
|
|
434
|
+
### LOW
|
|
435
|
+
|
|
436
|
+
**L1. Unify identifier normalisation (§3.9).** Document the difference between
|
|
437
|
+
`Util::key` and `FileLookup#key` (the `metanorma-collection ` strip) and rename one
|
|
438
|
+
so they are not both `key`. Consider a single `Identifier` helper.
|
|
439
|
+
**perf-risk: NEUTRAL.**
|
|
440
|
+
|
|
441
|
+
**L2. Document the manifest normalisation pipeline order (§3.8).** Add a comment to
|
|
442
|
+
`manifest_postprocess` (`manifest.rb:23`) stating why the 7 passes run in that order
|
|
443
|
+
and which depend on which.
|
|
444
|
+
**perf-risk: NEUTRAL.**
|
|
445
|
+
|
|
446
|
+
**L3. Add post-condition comments (not assertions) to each `update_xrefs` pass
|
|
447
|
+
(§3.10)** describing the tree state each pass guarantees on exit.
|
|
448
|
+
**perf-risk: NEUTRAL.**
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 5. Do NOT do this — refactors that would reintroduce bottlenecks
|
|
453
|
+
|
|
454
|
+
- **Do NOT replace the precomputed `:anchors` / `:anchors_lookup` / `:ids` tables
|
|
455
|
+
with on-demand xpath.** These are built once in `bibdata_extract`
|
|
456
|
+
(`filelookup.rb:87-93`) / `read_anchors` and consumed in the `update_anchors`
|
|
457
|
+
**bottleneck** (`fileparse.rb:196-213`) and `update_anchor_create_loc`
|
|
458
|
+
(`fileparse.rb:229-237`). Re-xpathing the target document per eref would turn an
|
|
459
|
+
O(1) lookup into O(anchors × erefs) re-traversal. The flattened `:anchors_lookup`
|
|
460
|
+
set exists specifically to avoid that.
|
|
461
|
+
|
|
462
|
+
- **Do NOT remove the hoist of `locate_internal_refs` out of the per-file loop**
|
|
463
|
+
(`fileprocess.rb:61`). Computing the internal-ref graph once and threading it in
|
|
464
|
+
is what keeps the loop O(files) and not O(files²). Folding it back inside
|
|
465
|
+
`update_xrefs` would re-derive the whole graph per file.
|
|
466
|
+
|
|
467
|
+
- **Do NOT "tidy up" by re-parsing XML where a parsed tree is already in hand, or
|
|
468
|
+
by parsing more than necessary.** The code already pays for ~3 re-parses per
|
|
469
|
+
source file (§3.1); that is the *floor* the maintainer has tuned to, not an
|
|
470
|
+
invitation to add more. Conversely, if consolidating §3.1 ever looks tempting,
|
|
471
|
+
treat caching parsed trees as a **measured** change — naively memoising every
|
|
472
|
+
parsed Nokogiri document for the whole render would blow memory on large
|
|
473
|
+
collections (these docs are huge; note the `&:huge` parse flag everywhere). Any
|
|
474
|
+
parse-caching must be scoped and benchmarked. **perf-risk on this whole area:
|
|
475
|
+
HIGH either direction.**
|
|
476
|
+
|
|
477
|
+
- **Do NOT bump the sectionsplit thread pool back to 4 without measuring**
|
|
478
|
+
(`sectionsplit.rb:48` is deliberately `FixedThreadPool.new(1)`; the size-4 variant
|
|
479
|
+
and `section_split_instance_threads` are commented out at
|
|
480
|
+
`filelookup_sectionsplit.rb:32`). Nokogiri tree mutation across threads is not
|
|
481
|
+
obviously safe here and the pools were dialled to 1 for a reason — likely
|
|
482
|
+
correctness or contention. Treat re-parallelising as a separate, carefully-tested
|
|
483
|
+
investigation, not a cleanup.
|
|
484
|
+
|
|
485
|
+
- **Do NOT convert the `@files` hash to per-access object allocation** (see H3
|
|
486
|
+
caveat). The wrapper, if introduced, must be one allocation per entry, and the
|
|
487
|
+
bottleneck loops should stay on raw reads until profiled.
|
|
488
|
+
|
|
489
|
+
- **Do NOT deep-clone Nokogiri trees between passes to "isolate" them.** The
|
|
490
|
+
in-place mutation (§3.10) is ugly but cheap; cloning the tree per pass to get
|
|
491
|
+
immutability would multiply parse/serialise cost across every document.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Appendix — key file:line index
|
|
496
|
+
|
|
497
|
+
| Concern | Location |
|
|
498
|
+
|---|---|
|
|
499
|
+
| Render spine | `renderer.rb:144-153` |
|
|
500
|
+
| File table built + sectionsplit expanded | `renderer.rb:81-82` |
|
|
501
|
+
| Per-file compile loop | `fileprocess.rb:59-84` |
|
|
502
|
+
| The three ref passes | `fileparse.rb:17-32` |
|
|
503
|
+
| Semantic/Presentation pivot (`sso`) | `fileparse.rb:34-39` |
|
|
504
|
+
| Internal-ref graph (double re-parse) | `fileprocess.rb:87-168` |
|
|
505
|
+
| `update_anchors` bottleneck | `fileparse.rb:195-213` |
|
|
506
|
+
| Attachment-link fix (`referencing_html_location`) | `fileparse.rb:208-232` |
|
|
507
|
+
| Sectionsplit core | `sectionsplit.rb:41-175` |
|
|
508
|
+
| Flat-XML / dir-HTML invariant | `sectionsplit.rb:166-174`, `filelookup.rb:359-369` |
|
|
509
|
+
| Sectionsplit @files expansion | `filelookup_sectionsplit.rb:6-130` |
|
|
510
|
+
| `@files` entry key comments | `filelookup.rb:102-107,128-134` |
|
|
511
|
+
| Duplicated FileLookup methods | `filelookup/utils.rb` vs `base.rb` vs `filelookup.rb` |
|
|
512
|
+
| Path math scattered | `filelocation.rb`, `utils.rb:91`, `filelookup.rb:145-312`, `fileparse.rb:208-232` |
|
|
513
|
+
| Manifest normalisation pipeline | `manifest.rb:23-32` |
|
|
514
|
+
| Shadowed method (dead) | `renderer.rb:94` vs `:108` |
|
|
515
|
+
|
|
516
|
+
🤖
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metanorma
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: asciidoctor
|
|
@@ -212,9 +211,9 @@ email:
|
|
|
212
211
|
executables: []
|
|
213
212
|
extensions: []
|
|
214
213
|
extra_rdoc_files:
|
|
215
|
-
- README.adoc
|
|
216
214
|
- CHANGELOG.adoc
|
|
217
215
|
- LICENSE.txt
|
|
216
|
+
- README.adoc
|
|
218
217
|
files:
|
|
219
218
|
- ".envrc"
|
|
220
219
|
- ".gitignore"
|
|
@@ -228,6 +227,7 @@ files:
|
|
|
228
227
|
- README.adoc
|
|
229
228
|
- lib/metanorma.rb
|
|
230
229
|
- lib/metanorma/array_monkeypatch.rb
|
|
230
|
+
- lib/metanorma/collection/artifact_store.rb
|
|
231
231
|
- lib/metanorma/collection/collection.rb
|
|
232
232
|
- lib/metanorma/collection/config/bibdata.rb
|
|
233
233
|
- lib/metanorma/collection/config/compile_options.rb
|
|
@@ -238,7 +238,6 @@ files:
|
|
|
238
238
|
- lib/metanorma/collection/config/manifest.rb
|
|
239
239
|
- lib/metanorma/collection/config/namespaces.rb
|
|
240
240
|
- lib/metanorma/collection/document/document.rb
|
|
241
|
-
- lib/metanorma/collection/filelookup/base.rb
|
|
242
241
|
- lib/metanorma/collection/filelookup/filelookup.rb
|
|
243
242
|
- lib/metanorma/collection/filelookup/filelookup_sectionsplit.rb
|
|
244
243
|
- lib/metanorma/collection/filelookup/utils.rb
|
|
@@ -274,11 +273,11 @@ files:
|
|
|
274
273
|
- lib/metanorma/util/worker_pool.rb
|
|
275
274
|
- lib/metanorma/version.rb
|
|
276
275
|
- metanorma.gemspec
|
|
276
|
+
- plans/collection-rendering-architecture-review.md
|
|
277
277
|
homepage: https://github.com/metanorma/metanorma
|
|
278
278
|
licenses:
|
|
279
279
|
- BSD-2-Clause
|
|
280
280
|
metadata: {}
|
|
281
|
-
post_install_message:
|
|
282
281
|
rdoc_options: []
|
|
283
282
|
require_paths:
|
|
284
283
|
- lib
|
|
@@ -286,15 +285,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
286
285
|
requirements:
|
|
287
286
|
- - ">="
|
|
288
287
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 3.
|
|
288
|
+
version: 3.3.0
|
|
290
289
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
291
290
|
requirements:
|
|
292
291
|
- - ">="
|
|
293
292
|
- !ruby/object:Gem::Version
|
|
294
293
|
version: '0'
|
|
295
294
|
requirements: []
|
|
296
|
-
rubygems_version:
|
|
297
|
-
signing_key:
|
|
295
|
+
rubygems_version: 4.0.10
|
|
298
296
|
specification_version: 4
|
|
299
297
|
summary: Metanorma is the standard of standards; the metanorma gem allows you to create
|
|
300
298
|
any standard document type supported by Metanorma.
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
module Metanorma
|
|
2
|
-
class Collection
|
|
3
|
-
class FileLookup
|
|
4
|
-
# are references to the file to be linked to a file in the collection,
|
|
5
|
-
# or externally? Determines whether file suffix anchors are to be used
|
|
6
|
-
def url?(ident)
|
|
7
|
-
data = get(ident) or return false
|
|
8
|
-
data[:url]
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def key(ident)
|
|
12
|
-
@c.decode(ident).gsub(/(\p{Zs})+/, " ")
|
|
13
|
-
.sub(/^metanorma-collection /, "")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def keys
|
|
17
|
-
@files.keys
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def get(ident, attr = nil)
|
|
21
|
-
if attr then @files[key(ident)][attr]
|
|
22
|
-
else @files[key(ident)]
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def set(ident, attr, value)
|
|
27
|
-
@files[key(ident)][attr] = value
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def each
|
|
31
|
-
@files.each
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def each_with_index
|
|
35
|
-
@files.each_with_index
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def ns(xpath)
|
|
39
|
-
@isodoc.ns(xpath)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|