metanorma-document 0.2.0 → 0.2.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.
@@ -17,43 +17,33 @@ module Metanorma
17
17
  class BaseRenderer
18
18
  LOGO_DIR = File.expand_path("../../../data/logos", __dir__)
19
19
 
20
- # Map legacy XML class names to clean, semantic names.
21
- CLASS_MAP = {
22
- "zzSTDTitle1" => "doc-title",
23
- "coverpage_docnumber" => "cover-doc-id",
24
- "coverpage_docstage" => "cover-stage",
25
- "doctitle-en" => "cover-title",
26
- "ForewordTitle" => "foreword-title",
27
- "IntroTitle" => "intro-title",
28
- "Annex" => "annex-title",
29
- "Section3" => "section-sub",
30
- "TermNum" => "term-number",
31
- "Terms" => "term-name",
32
- "DeprecatedTerms" => "term-deprecated",
33
- "domain" => "term-domain",
34
- "boldtitle" => "bold-title",
35
- "note_label" => "note-label",
36
- "termnote_label" => "term-note-label",
37
- "example_label" => "example-label",
38
- "stddocNumber" => "std-doc-number",
39
- "stdyear" => "std-year",
40
- "sourcecode-name" => "code-name",
20
+ # HTML-specific class names for inline spans, keyed by the XML span role.
21
+ # The XML class_attr is INPUT only — we never emit it in HTML.
22
+ SPAN_ROLE_CLASSES = {
23
+ "boldtitle" => "title-text",
24
+ "nonboldtitle" => "subtitle-text",
25
+ "citeapp" => "xref-app",
26
+ "citefig" => "xref-fig",
27
+ "citesec" => "xref-section",
28
+ "citetbl" => "xref-table",
29
+ "fmt-autonum-delim" => "number-delim",
41
30
  "fmt-caption-label" => "caption-label",
42
- "fmt-autonum-delim" => "autonum-delim",
43
- "fmt-element-name" => "element-name",
44
31
  "fmt-caption-delim" => "caption-delim",
45
- "smallcap" => "small-caps",
46
- "obligation" => "obligation-text",
47
- "tableblock" => "table-block",
48
- "Biblio" => "biblio-entry",
49
- "Note" => "note-block",
50
- "source" => "term-source",
51
- "nonboldtitle" => "doc-subtitle",
52
- "stddocPartNumber" => "doc-part-number",
53
- "stddocTitle" => "doc-part-title",
54
- "std_publisher" => "doc-publisher",
55
- "stdpublisher" => "doc-publisher-name",
32
+ "fmt-element-name" => "element-label",
33
+ "fmt-comma" => "comma",
34
+ "fmt-conn" => "connector",
35
+ "fmt-label-delim" => "label-delim",
36
+ "fmt-obligation" => "obligation-text",
37
+ "fmt-xref-container" => "xref-container",
56
38
  "fmt-xref-label" => "xref-label",
39
+ "std_publisher" => "ref-publisher",
40
+ "stdpublisher" => "ref-publisher-name",
41
+ "stddocNumber" => "ref-doc-number",
42
+ "stddocTitle" => "ref-title",
43
+ "stddocPartNumber" => "ref-part-number",
44
+ "stdyear" => "ref-year",
45
+ "date" => "date",
46
+ "smallcap" => "small-caps",
57
47
  }.freeze
58
48
 
59
49
  METANORMA_LOGO = "metanorma-logo.svg"
@@ -71,6 +61,40 @@ module Metanorma
71
61
 
72
62
  # --- Public API ---
73
63
 
64
+ # Facade object for Drops to call renderer methods without exposing
65
+ # the full private interface. Delegates to the renderer internally.
66
+ class RendererContext
67
+ def initialize(renderer)
68
+ @renderer = renderer
69
+ end
70
+
71
+ def safe_attr(obj, method_name) = @renderer.send(:safe_attr, obj, method_name)
72
+ def escape_html(text) = @renderer.send(:escape_html, text)
73
+ def extract_block_label(block, default) = @renderer.send(:extract_block_label, block, default)
74
+ def extract_plain_text(node) = @renderer.send(:extract_plain_text, node)
75
+ def capture_output(&) = @renderer.send(:capture_output, &)
76
+ def render_paragraph(p) = @renderer.send(:render_paragraph, p)
77
+ def render_mixed_inline(node) = @renderer.send(:render_mixed_inline, node)
78
+ def render_inline_element(el) = @renderer.send(:render_inline_element, el)
79
+ def render_unordered_list(ul) = @renderer.send(:render_unordered_list, ul)
80
+ def render_ordered_list(ol) = @renderer.send(:render_ordered_list, ol)
81
+ def render_definition_list(dl) = @renderer.send(:render_definition_list, dl)
82
+ def render_sourcecode(sc) = @renderer.send(:render_sourcecode, sc)
83
+ def render_table(t) = @renderer.send(:render_table, t)
84
+ def render_figure(f) = @renderer.send(:render_figure, f)
85
+ def render_quote(q) = @renderer.send(:render_quote, q)
86
+ def render_formula(f) = @renderer.send(:render_formula, f)
87
+ def render_note(n) = @renderer.send(:render_note, n)
88
+ def render_image(img) = @renderer.send(:render_image, img)
89
+ def render_stem_content(stem) = @renderer.send(:render_stem_content, stem)
90
+ def register_figure_entry(...) = @renderer.send(:register_figure_entry, ...)
91
+ def render_liquid(template_name, assigns) = @renderer.send(:render_liquid, template_name, assigns)
92
+ end
93
+
94
+ def renderer_context
95
+ @renderer_context ||= RendererContext.new(self)
96
+ end
97
+
74
98
  def to_html
75
99
  @output
76
100
  end
@@ -132,16 +156,16 @@ module Metanorma
132
156
  footer = build_footer
133
157
 
134
158
  render_liquid("document.html.liquid", {
135
- "lang" => language,
136
- "title" => html_title,
137
- "font_url" => flavor_font_url,
138
- "styles" => build_styles,
139
- "header" => header,
140
- "toc" => toc_html,
141
- "body" => body,
142
- "footer" => footer,
143
- "scripts" => build_scripts,
144
- })
159
+ "lang" => language,
160
+ "title" => html_title,
161
+ "font_url" => flavor_font_url,
162
+ "styles" => build_styles,
163
+ "header" => header,
164
+ "toc" => toc_html,
165
+ "body" => body,
166
+ "footer" => footer,
167
+ "scripts" => build_scripts,
168
+ })
145
169
  end
146
170
 
147
171
  # --- Header and Footer ---
@@ -157,15 +181,15 @@ module Metanorma
157
181
  end
158
182
 
159
183
  render_liquid("_header.html.liquid", {
160
- "publisher_logos" => pub_logos,
161
- "doc_id" => display_id,
162
- "doc_title" => header_title_text,
163
- })
184
+ "publisher_logos" => pub_logos,
185
+ "doc_id" => display_id,
186
+ "doc_title" => header_title_text,
187
+ })
164
188
  end
165
189
 
166
190
  def header_title_text
167
191
  raw = html_title.to_s.split(" — ").first.to_s.gsub(/<[^>]+>/, "")
168
- raw.length > 60 ? raw[0, 57] + "..." : raw
192
+ raw.length > 60 ? "#{raw[0, 57]}..." : raw
169
193
  end
170
194
 
171
195
  # Reader controls — kept for backward compat with flavor renderers
@@ -205,13 +229,12 @@ module Metanorma
205
229
  svg = svg.sub(/\A\s*<!--.*?-->\s*/m, "")
206
230
  svg = svg.sub(/<path[^>]*style="fill:#e3000f[^"]*"[^>]*\/>/, "")
207
231
  svg = svg.sub(/<svg\s/, '<svg class="header-logo" ')
208
- if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
209
- svg = svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/, "\\1 height=\"#{height}\"")
210
- else
211
- svg = svg.sub(/(<svg\b)/, "\\1 height=\"#{height}\"")
212
- end
213
- svg = svg.sub(/(<svg[^>]*?)\swidth="[^"]*"/, '\1')
214
- svg
232
+ svg = if svg.match?(/<svg[^>]*\sheight="[^"]*"/)
233
+ svg.sub(/(<svg[^>]*?)(\sheight="[^"]*")/, "\\1 height=\"#{height}\"")
234
+ else
235
+ svg.sub(/(<svg\b)/, "\\1 height=\"#{height}\"")
236
+ end
237
+ svg.sub(/(<svg[^>]*?)\swidth="[^"]*"/, '\1')
215
238
  rescue StandardError
216
239
  nil
217
240
  end
@@ -219,9 +242,9 @@ module Metanorma
219
242
  def build_footer
220
243
  mn_logo = load_logo_svg(METANORMA_LOGO, height: 20)
221
244
  render_liquid("_footer.html.liquid", {
222
- "mn_logo" => mn_logo,
223
- "generated_at" => Time.now.strftime('%Y-%m-%d %H:%M'),
224
- })
245
+ "mn_logo" => mn_logo,
246
+ "generated_at" => Time.now.strftime("%Y-%m-%d %H:%M"),
247
+ })
225
248
  end
226
249
 
227
250
  # --- ToC generation ---
@@ -231,32 +254,32 @@ module Metanorma
231
254
  main_lines = if entries.empty?
232
255
  ["<li class=\"toc-empty\">No entries</li>"]
233
256
  else
234
- entries.map { |e|
257
+ entries.map do |e|
235
258
  id = e[:id].to_s
236
259
  text = escape_html(e[:text].to_s)
237
260
  lvl = e[:level]
238
261
  "<li class=\"toc-level-#{lvl}\"><a href=\"##{id}\" class=\"toc-link\" data-target=\"#{id}\">#{text}</a></li>"
239
- }
262
+ end
240
263
  end
241
264
 
242
265
  # List of Figures — at top of sidebar
243
266
  unless @figure_entries.empty?
244
267
  top_lines << "<li class=\"toc-list-header\" data-list=\"figures\"><button class=\"toc-list-toggle\" aria-expanded=\"false\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"1\" y=\"2\" width=\"14\" height=\"12\" rx=\"1\"/><circle cx=\"5\" cy=\"6.5\" r=\"1.5\"/><path d=\"M1 12l4-4 2 2 3-3 5 5\"/></svg> Figures <span class=\"toc-list-count\">(#{@figure_entries.size})</span></button></li>"
245
- @figure_entries.each { |f|
268
+ @figure_entries.each do |f|
246
269
  id = f[:id].to_s
247
270
  text = escape_html(f[:text].to_s)
248
271
  top_lines << "<li class=\"toc-list-item toc-figures\" style=\"display:none\"><a href=\"##{id}\" class=\"toc-link\" data-target=\"#{id}\">#{text}</a></li>"
249
- }
272
+ end
250
273
  end
251
274
 
252
275
  # List of Tables — at top of sidebar
253
276
  unless @table_entries.empty?
254
277
  top_lines << "<li class=\"toc-list-header\" data-list=\"tables\"><button class=\"toc-list-toggle\" aria-expanded=\"false\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"1\" y=\"2\" width=\"14\" height=\"12\" rx=\"1\"/><line x1=\"1\" y1=\"6\" x2=\"15\" y2=\"6\"/><line x1=\"1\" y1=\"10\" x2=\"15\" y2=\"10\"/><line x1=\"7\" y1=\"2\" x2=\"7\" y2=\"14\"/></svg> Tables <span class=\"toc-list-count\">(#{@table_entries.size})</span></button></li>"
255
- @table_entries.each { |t|
278
+ @table_entries.each do |t|
256
279
  id = t[:id].to_s
257
280
  text = escape_html(t[:text].to_s)
258
281
  top_lines << "<li class=\"toc-list-item toc-tables\" style=\"display:none\"><a href=\"##{id}\" class=\"toc-link\" data-target=\"#{id}\">#{text}</a></li>"
259
- }
282
+ end
260
283
  end
261
284
 
262
285
  top_lines << "<li class=\"toc-divider\"></li>" unless top_lines.empty?
@@ -307,12 +330,25 @@ module Metanorma
307
330
  end
308
331
 
309
332
  if node.is_a?(Lutaml::Model::Serializable)
310
- return true if (node.public_send(:fmt_title) rescue nil)
311
- return true if (node.public_send(:displayorder) rescue nil)
333
+ return true if begin
334
+ node.fmt_title
335
+ rescue StandardError
336
+ nil
337
+ end
338
+ return true if begin
339
+ node.displayorder
340
+ rescue StandardError
341
+ nil
342
+ end
312
343
 
313
344
  %i[preface sections annex bibliography].each do |attr|
314
- val = node.public_send(attr) rescue nil
345
+ val = begin
346
+ node.public_send(attr)
347
+ rescue StandardError
348
+ nil
349
+ end
315
350
  next unless val
351
+
316
352
  Array(val).each { |v| return true if check_presentation_markers(v) }
317
353
  end
318
354
 
@@ -405,6 +441,7 @@ module Metanorma
405
441
  indices = Hash.new(0)
406
442
  node.element_order.each do |el|
407
443
  next unless el.is_a?(Lutaml::Xml::Element)
444
+
408
445
  if el.text?
409
446
  parts << el.text_content.to_s
410
447
  elsif el.name == "tab"
@@ -434,42 +471,42 @@ module Metanorma
434
471
  parts << (t.is_a?(Array) ? t.join : t.to_s) if t
435
472
  end
436
473
 
437
- parts.join.strip.gsub(/\u00A0/, " ")
474
+ parts.join.strip.gsub("\u00A0", " ")
438
475
  end
439
476
 
440
477
  # Dispatch to the appropriate render method based on node class.
441
- def render(node, **opts)
478
+ def render(node, **)
442
479
  case node
443
480
  when Metanorma::Document::Components::Paragraphs::ParagraphBlock
444
- render_paragraph(node, **opts)
481
+ render_paragraph(node, **)
445
482
  when Metanorma::Document::Components::Tables::TableBlock
446
- render_table(node, **opts)
483
+ render_table(node, **)
447
484
  when Metanorma::Document::Components::Lists::UnorderedList
448
- render_unordered_list(node, **opts)
485
+ render_unordered_list(node, **)
449
486
  when Metanorma::Document::Components::Lists::OrderedList
450
- render_ordered_list(node, **opts)
487
+ render_ordered_list(node, **)
451
488
  when Metanorma::Document::Components::Lists::DefinitionList
452
- render_definition_list(node, **opts)
489
+ render_definition_list(node, **)
453
490
  when Metanorma::Document::Components::AncillaryBlocks::FigureBlock
454
- render_figure(node, **opts)
491
+ render_figure(node, **)
455
492
  when Metanorma::Document::Components::Blocks::NoteBlock
456
- render_note(node, **opts)
493
+ render_note(node, **)
457
494
  when Metanorma::Document::Components::AncillaryBlocks::ExampleBlock
458
- render_example(node, **opts)
495
+ render_example(node, **)
459
496
  when Metanorma::Document::Components::AncillaryBlocks::SourcecodeBlock
460
- render_sourcecode(node, **opts)
497
+ render_sourcecode(node, **)
461
498
  when Metanorma::Document::Components::AncillaryBlocks::FormulaBlock
462
- render_formula(node, **opts)
499
+ render_formula(node, **)
463
500
  when Metanorma::Document::Components::MultiParagraph::QuoteBlock
464
- render_quote(node, **opts)
501
+ render_quote(node, **)
465
502
  when Metanorma::Document::Components::MultiParagraph::AdmonitionBlock
466
- render_admonition(node, **opts)
503
+ render_admonition(node, **)
467
504
  when Metanorma::Document::Components::Sections::HierarchicalSection
468
- render_hierarchical_section(node, **opts)
505
+ render_hierarchical_section(node, **)
469
506
  when Metanorma::Document::Components::Sections::BasicSection
470
- render_basic_section(node, **opts)
507
+ render_basic_section(node, **)
471
508
  when Metanorma::Document::Components::Sections::ContentSection
472
- render_content_section(node, **opts)
509
+ render_content_section(node, **)
473
510
  when Metanorma::Document::Components::EmptyElements::PageBreakElement
474
511
  ""
475
512
  when Metanorma::Document::Components::IdElements::Bookmark
@@ -488,7 +525,7 @@ module Metanorma
488
525
  # --- Block-level rendering ---
489
526
 
490
527
  def render_paragraph(p, **_opts)
491
- attrs = element_attrs(id: safe_attr(p, :id), class: safe_attr(p, :class_attr), style: alignment_style(safe_attr(p, :alignment)))
528
+ attrs = element_attrs(id: safe_attr(p, :id), style: alignment_style(safe_attr(p, :alignment)))
492
529
  tag("p", attrs) { render_mixed_inline(p) }
493
530
  end
494
531
 
@@ -531,19 +568,21 @@ module Metanorma
531
568
  if table.colgroup&.col && !table.colgroup.col.empty?
532
569
  return table.colgroup.col.size
533
570
  end
571
+
534
572
  # Walk all rows to find max column count, accounting for colspan
535
573
  max_cols = 0
536
- [:thead, :tbody, :tfoot].each do |section|
574
+ %i[thead tbody tfoot].each do |section|
537
575
  sec = table.public_send(section)
538
576
  next unless sec&.tr
577
+
539
578
  sec.tr.each do |tr|
540
579
  cols = 0
541
- Array(tr.th).each { |th| cols += (th.colspan && th.colspan > 1) ? th.colspan : 1 }
542
- Array(tr.td).each { |td| cols += (td.colspan && td.colspan > 1) ? td.colspan : 1 }
580
+ Array(tr.th).each { |th| cols += th.colspan && th.colspan > 1 ? th.colspan : 1 }
581
+ Array(tr.td).each { |td| cols += td.colspan && td.colspan > 1 ? td.colspan : 1 }
543
582
  max_cols = cols if cols > max_cols
544
583
  end
545
584
  end
546
- max_cols > 0 ? max_cols : 1
585
+ max_cols.positive? ? max_cols : 1
547
586
  end
548
587
 
549
588
  def render_table_colgroup(colgroup)
@@ -564,6 +603,7 @@ module Metanorma
564
603
  @output << "<tr>"
565
604
  walked = walk_ordered(tr) do |type, obj|
566
605
  next unless type == :element
606
+
567
607
  render_table_cell(obj)
568
608
  end
569
609
  unless walked
@@ -575,7 +615,7 @@ module Metanorma
575
615
  end
576
616
 
577
617
  def render_unordered_list(ul, **_opts)
578
- attrs = element_attrs(id: safe_attr(ul, :id), class: safe_attr(ul, :class_attr))
618
+ attrs = element_attrs(id: safe_attr(ul, :id))
579
619
  tag("ul", attrs) do
580
620
  ul.listitem&.each { |li| render_list_item(li) }
581
621
  end
@@ -595,7 +635,7 @@ module Metanorma
595
635
  end
596
636
 
597
637
  def render_ordered_list(ol, **_opts)
598
- attrs = element_attrs(id: safe_attr(ol, :id), class: safe_attr(ol, :class_attr), start: safe_attr(ol, :start), type: safe_attr(ol, :type_attr))
638
+ attrs = element_attrs(id: safe_attr(ol, :id), start: safe_attr(ol, :start), type: safe_attr(ol, :type_attr))
599
639
  tag("ol", attrs) do
600
640
  ol.listitem&.each { |li| render_list_item(li) }
601
641
  end
@@ -634,7 +674,8 @@ module Metanorma
634
674
  children = []
635
675
 
636
676
  walk_ordered(section) do |type, obj|
637
- next if type == :text || type == :tab
677
+ next if %i[text tab].include?(type)
678
+
638
679
  children << obj
639
680
  end
640
681
 
@@ -643,6 +684,7 @@ module Metanorma
643
684
  supplementary_attrs.each do |attr|
644
685
  val = safe_attr(section, attr)
645
686
  next if val.nil?
687
+
646
688
  Array(val).each do |v|
647
689
  children << v unless children.include?(v)
648
690
  end
@@ -665,7 +707,11 @@ module Metanorma
665
707
 
666
708
  def sort_by_displayorder(children)
667
709
  children.sort_by do |node|
668
- order = node.displayorder rescue nil
710
+ order = begin
711
+ node.displayorder
712
+ rescue StandardError
713
+ nil
714
+ end
669
715
  order &&= order.to_i
670
716
  order || Float::INFINITY
671
717
  end
@@ -689,29 +735,8 @@ module Metanorma
689
735
  end
690
736
 
691
737
  def render_figure(figure, **_opts)
692
- attrs = element_attrs(id: safe_attr(figure, :id), class: "figure")
693
- fig_id = safe_attr(figure, :id)
694
- fig_name = safe_attr(figure, :fmt_name) || safe_attr(figure, :name)
695
- if fig_id && fig_name
696
- register_figure_entry(id: fig_id, text: extract_plain_text(fig_name))
697
- end
698
- tag("figure", attrs) do
699
- if figure.image
700
- render_image(figure.image)
701
- elsif safe_attr(figure, :source)
702
- @output << %(<img src="#{escape_html(figure.source)}" />)
703
- end
704
- render_video(figure.video) if safe_attr(figure, :video)
705
- render_audio(figure.audio) if safe_attr(figure, :audio)
706
- figure.figure&.each { |sub| render_figure(sub) }
707
- if safe_attr(figure, :name) || safe_attr(figure, :fmt_name)
708
- @output << "<figcaption>"
709
- render_inline_element(safe_attr(figure, :fmt_name) || figure.name)
710
- @output << "</figcaption>"
711
- end
712
- safe_attr(figure, :note)&.each { |n| render_note(n) }
713
- safe_attr(figure, :dl)&.then { |dl| render_definition_list(dl) }
714
- end
738
+ drop = Drops::FigureDrop.from_model(figure, renderer: renderer_context)
739
+ @output << render_liquid("_figure.html.liquid", { "block" => drop })
715
740
  end
716
741
 
717
742
  def render_image(image)
@@ -743,91 +768,23 @@ module Metanorma
743
768
  end
744
769
 
745
770
  def render_note(note, **_opts)
746
- attrs = element_attrs(id: safe_attr(note, :id), class: "note-block")
747
- tag("div", attrs) do
748
- label = extract_block_label(note, "NOTE")
749
- @output << %(<span class="note-label">#{escape_html(label)}</span>&nbsp;)
750
- if note.content && !note.content.empty?
751
- note.content.each { |para| render_paragraph(para) }
752
- else
753
- render_mixed_inline(note)
754
- end
755
- note.ul&.each { |ul| render_unordered_list(ul) }
756
- note.ol&.each { |ol| render_ordered_list(ol) }
757
- note.dl&.then { |dl| render_definition_list(dl) }
758
- end
771
+ drop = Drops::NoteDrop.from_model(note, renderer: renderer_context)
772
+ @output << render_liquid("_note.html.liquid", { "block" => drop })
759
773
  end
760
774
 
761
775
  def render_example(example, **_opts)
762
- attrs = element_attrs(id: safe_attr(example, :id), class: "example")
763
- tag("div", attrs) do
764
- label = extract_block_label(example, "EXAMPLE")
765
- @output << %(<span class="example-label">#{escape_html(label)}</span>&nbsp;)
766
- if example.paragraphs && !example.paragraphs.empty?
767
- example.paragraphs.each { |para| render_paragraph(para) }
768
- end
769
- example.ul&.each { |ul| render_unordered_list(ul) }
770
- example.ol&.each { |ol| render_ordered_list(ol) }
771
- example.dl&.each { |dl| render_definition_list(dl) } if example.dl
772
- example.sourcecode&.each { |sc| render_sourcecode(sc) }
773
- example.table&.each { |t| render_table(t) }
774
- example.figure&.each { |f| render_figure(f) }
775
- example.quote&.each { |q| render_quote(q) }
776
- example.formula&.each { |f| render_formula(f) }
777
- end
776
+ drop = Drops::ExampleDrop.from_model(example, renderer: renderer_context)
777
+ @output << render_liquid("_example.html.liquid", { "block" => drop })
778
778
  end
779
779
 
780
780
  def render_sourcecode(sc, **_opts)
781
- attrs = element_attrs(id: safe_attr(sc, :id), class: "sourcecode")
782
- lang = safe_attr(sc, :lang)
783
- tag("div", attrs) do
784
- if sc.name
785
- @output << "<p class=\"code-name\">"
786
- render_inline_element(sc.name)
787
- @output << "</p>"
788
- end
789
- code_attrs = lang ? %( lang="#{escape_html(lang)}") : ""
790
- @output << "<pre><code#{code_attrs}>"
791
- # Use body.content if available, else content, else text
792
- code_text = if sc.body && sc.body.content
793
- sc.body.content
794
- elsif sc.content
795
- sc.content
796
- else
797
- ""
798
- end
799
- # body.content from map_all_content may contain pre-escaped HTML
800
- # entities (&lt; etc); decode first to get raw text, then escape
801
- # for HTML output.
802
- raw_text = code_text.gsub("&lt;", "<").gsub("&gt;", ">").gsub("&amp;", "&").gsub("&quot;", "\"")
803
- @output << escape_html(raw_text)
804
- @output << "</code></pre>"
805
- end
781
+ drop = Drops::SourcecodeDrop.from_model(sc, renderer: renderer_context)
782
+ @output << render_liquid("_sourcecode.html.liquid", { "block" => drop })
806
783
  end
807
784
 
808
785
  def render_formula(formula, **_opts)
809
- attrs = element_attrs(id: safe_attr(formula, :id), class: "formula")
810
- tag("div", attrs) do
811
- @output << render_stem_content(formula.stem) if formula.stem
812
-
813
- # Render "where" clause from key element (non-presentation XML)
814
- if formula.key
815
- if formula.key.dl
816
- @output << "<p class=\"formula-where\">where</p>"
817
- render_definition_list(formula.key.dl)
818
- end
819
- formula.key.p&.each { |para| render_paragraph(para) }
820
- end
821
-
822
- formula.dl&.then { |dl| render_definition_list(dl) }
823
-
824
- name_el = safe_attr(formula, :fmt_name) || safe_attr(formula, :name)
825
- if name_el
826
- @output << "<span class=\"formula-number\">"
827
- render_inline_element(name_el)
828
- @output << "</span>"
829
- end
830
- end
786
+ drop = Drops::FormulaDrop.from_model(formula, renderer: renderer_context)
787
+ @output << render_liquid("_formula.html.liquid", { "block" => drop })
831
788
  end
832
789
 
833
790
  def render_quote(quote, **_opts)
@@ -845,12 +802,8 @@ module Metanorma
845
802
  end
846
803
 
847
804
  def render_admonition(admonition, **_opts)
848
- type = safe_attr(admonition, :type) || "note"
849
- attrs = element_attrs(id: safe_attr(admonition, :id), class: "admonition #{type}")
850
- tag("div", attrs) do
851
- @output << "<p class=\"admonition-title\">#{escape_html(type.capitalize)}</p>"
852
- admonition.paragraphs&.each { |para| render_paragraph(para) }
853
- end
805
+ drop = Drops::AdmonitionDrop.from_model(admonition, renderer: renderer_context)
806
+ @output << render_liquid("_admonition.html.liquid", { "block" => drop })
854
807
  end
855
808
 
856
809
  def render_bookmark(bookmark)
@@ -973,13 +926,14 @@ module Metanorma
973
926
  skip = {}
974
927
  node.element_order.each_with_index do |el, i|
975
928
  next unless el.element?
929
+
976
930
  next_tag = skip_after[el.name]
977
931
  next unless next_tag
978
932
 
979
933
  next_el = node.element_order[i + 1]
980
934
  if next_tag.nil?
981
935
  skip[i] = true
982
- elsif next_el && next_el.element? && next_el.name == next_tag
936
+ elsif next_el&.element? && next_el.name == next_tag
983
937
  skip[i] = true
984
938
  end
985
939
  end
@@ -1107,7 +1061,9 @@ module Metanorma
1107
1061
  # Source element — skip; rendered via fmt-xref in semx wrapper
1108
1062
  nil
1109
1063
  when Metanorma::Document::Components::Inline::SpanElement
1110
- attrs = element_attrs(style: safe_attr(element, :style), class: safe_attr(element, :class_attr))
1064
+ xml_class = safe_attr(element, :class_attr).to_s
1065
+ html_class = html_class_for_span(xml_class) unless xml_class.empty?
1066
+ attrs = element_attrs(style: safe_attr(element, :style), class: html_class)
1111
1067
  tag("span", attrs) { render_mixed_inline(element) }
1112
1068
  when Metanorma::Document::Components::Inline::FnElement
1113
1069
  render_fn(element)
@@ -1183,13 +1139,13 @@ module Metanorma
1183
1139
  texts = node.text
1184
1140
  if texts.is_a?(Array)
1185
1141
  texts.each do |t|
1186
- if t.is_a?(Metanorma::Document::Components::Inline::MathElement)
1187
- @output << t.content.to_s
1188
- elsif t.is_a?(Metanorma::Document::Components::Inline::AsciimathElement)
1189
- @output << %(<span class="stem">#{escape_html(Array(t.text).join)}</span>)
1190
- else
1191
- @output << escape_html(t.to_s)
1192
- end
1142
+ @output << if t.is_a?(Metanorma::Document::Components::Inline::MathElement)
1143
+ t.content.to_s
1144
+ elsif t.is_a?(Metanorma::Document::Components::Inline::AsciimathElement)
1145
+ %(<span class="stem">#{escape_html(Array(t.text).join)}</span>)
1146
+ else
1147
+ escape_html(t.to_s)
1148
+ end
1193
1149
  end
1194
1150
  elsif texts.is_a?(String)
1195
1151
  @output << escape_html(texts)
@@ -1239,6 +1195,7 @@ module Metanorma
1239
1195
  display_attrs.each do |attr|
1240
1196
  val = safe_attr(element, attr)
1241
1197
  next if val.nil?
1198
+
1242
1199
  if val.is_a?(Array)
1243
1200
  val.each do |v|
1244
1201
  if v.is_a?(Metanorma::Document::Components::Paragraphs::ParagraphBlock)
@@ -1261,7 +1218,7 @@ module Metanorma
1261
1218
  return semx_text unless first_word
1262
1219
 
1263
1220
  tail = output[-200..]
1264
- return semx_text unless tail && tail.rstrip.end_with?(first_word)
1221
+ return semx_text unless tail&.rstrip&.end_with?(first_word)
1265
1222
 
1266
1223
  semx_text.sub(/\A\s*#{Regexp.escape(first_word)}\s*/, "")
1267
1224
  end
@@ -1279,7 +1236,7 @@ module Metanorma
1279
1236
  doc.css("fmt-link").each do |el|
1280
1237
  target = el["target"] || el["href"]
1281
1238
  if target
1282
- display_text = target.sub(/\Amailto:/, "")
1239
+ display_text = target.delete_prefix("mailto:")
1283
1240
  a = doc.document.create_element("a", display_text, "href" => target)
1284
1241
  el.replace(a)
1285
1242
  else
@@ -1292,11 +1249,12 @@ module Metanorma
1292
1249
  doc.traverse do |node|
1293
1250
  next unless node.element?
1294
1251
  next unless %w[xref eref stem link].include?(node.name)
1252
+
1295
1253
  next_sib = node.next_sibling
1296
1254
  while next_sib.is_a?(Nokogiri::XML::Text) && next_sib.text.strip.empty?
1297
1255
  next_sib = next_sib.next_sibling
1298
1256
  end
1299
- next unless next_sib && next_sib.element? && next_sib.name == "semx"
1257
+ next unless next_sib&.element? && next_sib.name == "semx"
1300
1258
 
1301
1259
  deduplicate_semx_label(node, next_sib)
1302
1260
  node.remove
@@ -1305,6 +1263,10 @@ module Metanorma
1305
1263
  %w[semx fmt-xref].each do |tag|
1306
1264
  doc.css(tag).each { |el| el.replace(el.children) }
1307
1265
  end
1266
+ # Remap XML class names to HTML-specific class names
1267
+ doc.css("[class]").each do |el|
1268
+ el["class"] = el["class"].split(/\s+/).map { |c| html_class_for_span(c) }.join(" ")
1269
+ end
1308
1270
  doc.inner_html
1309
1271
  end
1310
1272
 
@@ -1328,13 +1290,13 @@ module Metanorma
1328
1290
 
1329
1291
  def render_link(link)
1330
1292
  target = safe_attr(link, :target) || safe_attr(link, :href)
1331
- attrs = element_attrs(href: target, id: safe_attr(link, :id), class: safe_attr(link, :class_attr))
1293
+ attrs = element_attrs(href: target, id: safe_attr(link, :id))
1332
1294
  tag("a", attrs) do
1333
1295
  content = safe_attr(link, :content)
1334
1296
  if content && !Array(content).join.strip.empty?
1335
1297
  render_mixed_inline(link)
1336
1298
  else
1337
- display_text = target.to_s.sub(/\Amailto:/, "")
1299
+ display_text = target.to_s.delete_prefix("mailto:")
1338
1300
  @output << escape_html(display_text)
1339
1301
  end
1340
1302
  end
@@ -1342,7 +1304,7 @@ module Metanorma
1342
1304
 
1343
1305
  def render_xref(xref)
1344
1306
  target = safe_attr(xref, :target) || safe_attr(xref, :to_attr)
1345
- attrs = element_attrs(href: "##{escape_html(target)}", id: safe_attr(xref, :id), class: safe_attr(xref, :class_attr))
1307
+ attrs = element_attrs(href: "##{escape_html(target)}", id: safe_attr(xref, :id))
1346
1308
  tag("a", attrs) { render_mixed_inline(xref) }
1347
1309
  end
1348
1310
 
@@ -1460,14 +1422,13 @@ module Metanorma
1460
1422
  attrs.each do |k, v|
1461
1423
  next if v.nil? || v == false || (v.is_a?(String) && v.empty?)
1462
1424
 
1463
- val = k == :class ? translate_class(v.to_s) : v.to_s
1464
- parts << %( #{k}="#{escape_html(val)}")
1425
+ parts << %( #{k}="#{escape_html(v.to_s)}")
1465
1426
  end
1466
1427
  parts.join
1467
1428
  end
1468
1429
 
1469
- def translate_class(class_str)
1470
- class_str.split(/\s+/).map { |c| CLASS_MAP[c] || c }.join(" ")
1430
+ def html_class_for_span(xml_class)
1431
+ SPAN_ROLE_CLASSES[xml_class] || "span-#{xml_class}"
1471
1432
  end
1472
1433
 
1473
1434
  def block_element?(obj)
@@ -1503,7 +1464,7 @@ module Metanorma
1503
1464
  secondary: safe_attr(element, :secondary)&.to_s&.strip,
1504
1465
  tertiary: safe_attr(element, :tertiary)&.to_s&.strip,
1505
1466
  target_id: @current_section_id,
1506
- target_text: @current_section_number
1467
+ target_text: @current_section_number,
1507
1468
  )
1508
1469
  rescue StandardError
1509
1470
  nil