coradoc-html 1.1.7 → 1.1.13

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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/html/config.rb +36 -12
  3. data/lib/coradoc/html/converter_base.rb +26 -68
  4. data/lib/coradoc/html/drop/annotation_drop.rb +31 -0
  5. data/lib/coradoc/html/drop/base.rb +77 -0
  6. data/lib/coradoc/html/drop/bibliography_drop.rb +15 -0
  7. data/lib/coradoc/html/drop/bibliography_entry_drop.rb +24 -0
  8. data/lib/coradoc/html/drop/block_drop.rb +69 -0
  9. data/lib/coradoc/html/drop/definition_item_drop.rb +36 -0
  10. data/lib/coradoc/html/drop/definition_list_drop.rb +15 -0
  11. data/lib/coradoc/html/drop/document_drop.rb +52 -0
  12. data/lib/coradoc/html/drop/drop_factory.rb +73 -0
  13. data/lib/coradoc/html/drop/footnote_drop.rb +24 -0
  14. data/lib/coradoc/html/drop/image_drop.rb +35 -0
  15. data/lib/coradoc/html/drop/inline_element_drop.rb +64 -0
  16. data/lib/coradoc/html/drop/list_block_drop.rb +23 -0
  17. data/lib/coradoc/html/drop/list_item_drop.rb +20 -0
  18. data/lib/coradoc/html/drop/table_cell_drop.rb +35 -0
  19. data/lib/coradoc/html/drop/table_drop.rb +15 -0
  20. data/lib/coradoc/html/drop/table_row_drop.rb +23 -0
  21. data/lib/coradoc/html/drop/term_drop.rb +24 -0
  22. data/lib/coradoc/html/drop/text_content_drop.rb +15 -0
  23. data/lib/coradoc/html/drop/toc_drop.rb +15 -0
  24. data/lib/coradoc/html/drop/toc_entry_drop.rb +32 -0
  25. data/lib/coradoc/html/escape.rb +29 -0
  26. data/lib/coradoc/html/input/cleaner.rb +4 -33
  27. data/lib/coradoc/html/input/config.rb +4 -3
  28. data/lib/coradoc/html/input/converters/a.rb +8 -19
  29. data/lib/coradoc/html/input/converters/aside.rb +4 -5
  30. data/lib/coradoc/html/input/converters/audio.rb +8 -35
  31. data/lib/coradoc/html/input/converters/base.rb +29 -27
  32. data/lib/coradoc/html/input/converters/blockquote.rb +4 -2
  33. data/lib/coradoc/html/input/converters/br.rb +4 -4
  34. data/lib/coradoc/html/input/converters/bypass.rb +68 -67
  35. data/lib/coradoc/html/input/converters/code.rb +7 -5
  36. data/lib/coradoc/html/input/converters/div.rb +4 -4
  37. data/lib/coradoc/html/input/converters/dl.rb +3 -25
  38. data/lib/coradoc/html/input/converters/drop.rb +13 -13
  39. data/lib/coradoc/html/input/converters/em.rb +5 -3
  40. data/lib/coradoc/html/input/converters/figure.rb +3 -26
  41. data/lib/coradoc/html/input/converters/h.rb +9 -11
  42. data/lib/coradoc/html/input/converters/head.rb +5 -4
  43. data/lib/coradoc/html/input/converters/hr.rb +4 -5
  44. data/lib/coradoc/html/input/converters/img.rb +4 -9
  45. data/lib/coradoc/html/input/converters/li.rb +3 -1
  46. data/lib/coradoc/html/input/converters/mark.rb +3 -1
  47. data/lib/coradoc/html/input/converters/markup.rb +4 -8
  48. data/lib/coradoc/html/input/converters/math.rb +7 -14
  49. data/lib/coradoc/html/input/converters/media_base.rb +50 -0
  50. data/lib/coradoc/html/input/converters/ol.rb +6 -8
  51. data/lib/coradoc/html/input/converters/p.rb +43 -34
  52. data/lib/coradoc/html/input/converters/pass_through.rb +2 -4
  53. data/lib/coradoc/html/input/converters/positional_formatting.rb +37 -0
  54. data/lib/coradoc/html/input/converters/pre.rb +3 -3
  55. data/lib/coradoc/html/input/converters/q.rb +6 -3
  56. data/lib/coradoc/html/input/converters/strong.rb +4 -2
  57. data/lib/coradoc/html/input/converters/sub.rb +7 -23
  58. data/lib/coradoc/html/input/converters/sup.rb +7 -23
  59. data/lib/coradoc/html/input/converters/table.rb +3 -1
  60. data/lib/coradoc/html/input/converters/td.rb +4 -30
  61. data/lib/coradoc/html/input/converters/text.rb +4 -3
  62. data/lib/coradoc/html/input/converters/tr.rb +3 -2
  63. data/lib/coradoc/html/input/converters/video.rb +14 -36
  64. data/lib/coradoc/html/input/converters.rb +17 -35
  65. data/lib/coradoc/html/input/html_converter.rb +2 -74
  66. data/lib/coradoc/html/input/plugin.rb +8 -50
  67. data/lib/coradoc/html/input/plugins/plateau.rb +4 -19
  68. data/lib/coradoc/html/input/postprocessor.rb +3 -9
  69. data/lib/coradoc/html/input.rb +26 -8
  70. data/lib/coradoc/html/layout_renderer.rb +163 -0
  71. data/lib/coradoc/html/output.rb +6 -12
  72. data/lib/coradoc/html/renderer.rb +84 -350
  73. data/lib/coradoc/html/section_numberable.rb +9 -0
  74. data/lib/coradoc/html/spa.rb +29 -270
  75. data/lib/coradoc/html/static.rb +29 -238
  76. data/lib/coradoc/html/template_caching.rb +31 -0
  77. data/lib/coradoc/html/template_config.rb +11 -70
  78. data/lib/coradoc/html/template_helpers.rb +39 -31
  79. data/lib/coradoc/html/template_locator.rb +17 -11
  80. data/lib/coradoc/html/theme.rb +1 -7
  81. data/lib/coradoc/html/title_text.rb +57 -0
  82. data/lib/coradoc/html/toc_builder.rb +105 -0
  83. data/lib/coradoc/html/toc_serializer.rb +33 -0
  84. data/lib/coradoc/html/transform/from_core_model.rb +13 -12
  85. data/lib/coradoc/html/transform/to_core_model.rb +10 -12
  86. data/lib/coradoc/html/version.rb +1 -1
  87. data/lib/coradoc/html.rb +43 -88
  88. metadata +37 -70
  89. data/lib/coradoc/html/base.rb +0 -157
  90. data/lib/coradoc/html/converters/admonition.rb +0 -180
  91. data/lib/coradoc/html/converters/attribute.rb +0 -68
  92. data/lib/coradoc/html/converters/attribute_reference.rb +0 -60
  93. data/lib/coradoc/html/converters/audio.rb +0 -165
  94. data/lib/coradoc/html/converters/base.rb +0 -615
  95. data/lib/coradoc/html/converters/bibliography.rb +0 -82
  96. data/lib/coradoc/html/converters/bibliography_entry.rb +0 -108
  97. data/lib/coradoc/html/converters/block_image.rb +0 -72
  98. data/lib/coradoc/html/converters/bold.rb +0 -34
  99. data/lib/coradoc/html/converters/break.rb +0 -32
  100. data/lib/coradoc/html/converters/comment_block.rb +0 -42
  101. data/lib/coradoc/html/converters/comment_line.rb +0 -54
  102. data/lib/coradoc/html/converters/cross_reference.rb +0 -59
  103. data/lib/coradoc/html/converters/document.rb +0 -108
  104. data/lib/coradoc/html/converters/example.rb +0 -114
  105. data/lib/coradoc/html/converters/highlight.rb +0 -34
  106. data/lib/coradoc/html/converters/include.rb +0 -68
  107. data/lib/coradoc/html/converters/inline_image.rb +0 -41
  108. data/lib/coradoc/html/converters/italic.rb +0 -34
  109. data/lib/coradoc/html/converters/line_break.rb +0 -31
  110. data/lib/coradoc/html/converters/link.rb +0 -46
  111. data/lib/coradoc/html/converters/list_item.rb +0 -75
  112. data/lib/coradoc/html/converters/listing.rb +0 -99
  113. data/lib/coradoc/html/converters/literal.rb +0 -102
  114. data/lib/coradoc/html/converters/monospace.rb +0 -34
  115. data/lib/coradoc/html/converters/open.rb +0 -78
  116. data/lib/coradoc/html/converters/ordered.rb +0 -53
  117. data/lib/coradoc/html/converters/paragraph.rb +0 -46
  118. data/lib/coradoc/html/converters/quote.rb +0 -113
  119. data/lib/coradoc/html/converters/reviewer_comment.rb +0 -74
  120. data/lib/coradoc/html/converters/reviewer_note.rb +0 -134
  121. data/lib/coradoc/html/converters/section.rb +0 -90
  122. data/lib/coradoc/html/converters/sidebar.rb +0 -113
  123. data/lib/coradoc/html/converters/source.rb +0 -137
  124. data/lib/coradoc/html/converters/source_code.rb +0 -16
  125. data/lib/coradoc/html/converters/span.rb +0 -61
  126. data/lib/coradoc/html/converters/strikethrough.rb +0 -34
  127. data/lib/coradoc/html/converters/subscript.rb +0 -34
  128. data/lib/coradoc/html/converters/superscript.rb +0 -34
  129. data/lib/coradoc/html/converters/table.rb +0 -85
  130. data/lib/coradoc/html/converters/table_cell.rb +0 -203
  131. data/lib/coradoc/html/converters/table_row.rb +0 -45
  132. data/lib/coradoc/html/converters/template_html_converter.rb +0 -105
  133. data/lib/coradoc/html/converters/term.rb +0 -58
  134. data/lib/coradoc/html/converters/text_element.rb +0 -44
  135. data/lib/coradoc/html/converters/underline.rb +0 -34
  136. data/lib/coradoc/html/converters/unordered.rb +0 -47
  137. data/lib/coradoc/html/converters/verse.rb +0 -105
  138. data/lib/coradoc/html/converters/video.rb +0 -179
  139. data/lib/coradoc/html/element_mapping.rb +0 -210
  140. data/lib/coradoc/html/entity.rb +0 -137
  141. data/lib/coradoc/html/input/converters/ignore.rb +0 -22
  142. data/lib/coradoc/html/input/converters/th.rb +0 -20
  143. data/lib/coradoc/html/theme/base.rb +0 -231
  144. data/lib/coradoc/html/theme/classic_renderer.rb +0 -390
  145. data/lib/coradoc/html/theme/modern/components/ui_components.rb +0 -344
  146. data/lib/coradoc/html/theme/modern/css_generator.rb +0 -311
  147. data/lib/coradoc/html/theme/modern/javascript_generator.rb +0 -314
  148. data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +0 -382
  149. data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +0 -164
  150. data/lib/coradoc/html/theme/modern/vue_template_generator.rb +0 -374
  151. data/lib/coradoc/html/theme/modern_renderer.rb +0 -250
  152. data/lib/coradoc/html/theme/registry.rb +0 -153
@@ -4,45 +4,32 @@ require 'pathname'
4
4
 
5
5
  module Coradoc
6
6
  module Html
7
- # Configuration for the Liquid template system
7
+ # Configuration for the Liquid template system.
8
8
  #
9
- # This class manages template directories and provides utilities
10
- # for template discovery and customization.
9
+ # Delegates template lookup to TemplateLocator for consistent behavior
10
+ # across global configuration and per-renderer usage.
11
11
  #
12
12
  # @example Global configuration
13
13
  # Coradoc::Html.configure do |config|
14
14
  # config.template_dirs = ["/path/to/custom/templates"]
15
15
  # end
16
- #
17
- # @example Per-render configuration
18
- # Coradoc::Html.serialize(document, template_dirs: ["/custom/templates"])
19
- #
20
16
  class TemplateConfig
21
- # Default template directory within the gem
22
- DEFAULT_TEMPLATE_DIR = Pathname.new(File.join(
23
- File.dirname(__FILE__), 'templates', 'core_model'
24
- )).freeze
17
+ DEFAULT_TEMPLATE_DIR = TemplateLocator::DEFAULT_TEMPLATE_DIR
25
18
 
26
- # @return [Array<Pathname>] List of user-provided template directories
27
19
  attr_accessor :template_dirs
28
20
 
29
- # Initialize a new configuration
30
- #
31
- # @param template_dirs [Array<String, Pathname>] Custom template directories
32
21
  def initialize(template_dirs: [])
33
22
  @template_dirs = Array(template_dirs).map { |dir| Pathname.new(dir) }
34
23
  end
35
24
 
36
- # Get all template directories (user + default)
37
- #
38
- # @return [Array<Pathname>] All template directories in search order
25
+ def locator
26
+ @locator ||= TemplateLocator.new(user_dirs: @template_dirs)
27
+ end
28
+
39
29
  def all_template_dirs
40
30
  @template_dirs + [DEFAULT_TEMPLATE_DIR]
41
31
  end
42
32
 
43
- # List all available default templates
44
- #
45
- # @return [Array<Symbol>] List of template names (without .liquid extension)
46
33
  def self.available_templates
47
34
  @available_templates ||= begin
48
35
  return [] unless DEFAULT_TEMPLATE_DIR.exist?
@@ -54,48 +41,24 @@ module Coradoc
54
41
  end
55
42
  end
56
43
 
57
- # Get the path to a specific default template
58
- #
59
- # @param name [Symbol, String] Template name (e.g., :bibliography)
60
- # @return [Pathname, nil] Path to the template file, or nil if not found
61
44
  def self.template_path_for(name)
62
45
  path = DEFAULT_TEMPLATE_DIR.join("#{name}.liquid")
63
46
  path.exist? ? path : nil
64
47
  end
65
48
 
66
- # Check if a template exists
67
- #
68
- # @param name [Symbol, String] Template name
69
- # @return [Boolean] True if template exists in any directory
70
49
  def template_exists?(name)
71
- all_template_dirs.any? do |dir|
72
- dir.join("#{name}.liquid").exist?
73
- end
50
+ locator.exists?(name.to_s)
74
51
  end
75
52
 
76
- # Find a template by name
77
- #
78
- # @param name [Symbol, String] Template name
79
- # @return [Pathname, nil] Path to the first matching template
80
53
  def find_template(name)
81
- all_template_dirs.each do |dir|
82
- path = dir.join("#{name}.liquid")
83
- return path if path.exist?
84
- end
85
- nil
54
+ locator.find(name.to_s)
86
55
  end
87
56
 
88
- # Reset configuration to defaults
89
- #
90
- # @return [void]
91
57
  def reset!
92
58
  @template_dirs = []
59
+ @locator = nil
93
60
  end
94
61
 
95
- # Create a copy of this configuration with additional directories
96
- #
97
- # @param additional_dirs [Array<String, Pathname>] Extra directories
98
- # @return [TemplateConfig] New configuration with merged directories
99
62
  def with_dirs(additional_dirs)
100
63
  self.class.new(
101
64
  template_dirs: @template_dirs + Array(additional_dirs).map { |d| Pathname.new(d) }
@@ -105,44 +68,22 @@ module Coradoc
105
68
 
106
69
  # Module-level configuration storage
107
70
  class << self
108
- # Get the global configuration
109
- #
110
- # @return [TemplateConfig] The global configuration
111
71
  def configuration
112
72
  @configuration ||= TemplateConfig.new
113
73
  end
114
74
 
115
- # Configure the template system
116
- #
117
- # @yield [TemplateConfig] Yields the configuration object
118
- # @return [void]
119
- #
120
- # @example
121
- # Coradoc::Html.configure do |config|
122
- # config.template_dirs = ["/path/to/templates"]
123
- # end
124
75
  def configure
125
76
  yield(configuration) if block_given?
126
77
  end
127
78
 
128
- # Reset configuration to defaults
129
- #
130
- # @return [void]
131
79
  def reset_configuration!
132
80
  @configuration = nil
133
81
  end
134
82
 
135
- # List all available default templates
136
- #
137
- # @return [Array<Symbol>] List of template names
138
83
  def available_templates
139
84
  TemplateConfig.available_templates
140
85
  end
141
86
 
142
- # Get the path to a default template
143
- #
144
- # @param name [Symbol, String] Template name
145
- # @return [Pathname, nil] Path to template or nil
146
87
  def template_path_for(name)
147
88
  TemplateConfig.template_path_for(name)
148
89
  end
@@ -6,53 +6,61 @@ module Coradoc
6
6
  module Html
7
7
  # Liquid filters for template rendering
8
8
  module TemplateFilters
9
- # Render a CoreModel element by looking up its template
9
+ # Render a CoreModel element or Drop by looking up its template.
10
10
  #
11
11
  # Usage in templates:
12
12
  # {{ child | render_element }}
13
13
  # {% for item in children %}{{ item | render_element }}{% endfor %}
14
14
  #
15
- def render_element(input, renderer = nil)
15
+ def render_element(input)
16
16
  return '' if input.nil?
17
- return input.map { |i| render_element(i, renderer) }.join("\n") if input.is_a?(Array)
18
-
19
- # Get the renderer from context registers
20
- renderer ||= @context.registers[:renderer]
21
-
22
- # If input is a Liquid::Drop, extract the original model object.
23
- # Liquid::Drop stores the wrapped object as @object with no public
24
- # reader — this is the only way to unwrap it.
25
- original = if input.is_a?(::Liquid::Drop)
26
- input.instance_variable_get(:@object)
27
- else
28
- input
29
- end
30
-
31
- # Renderer is always a Coradoc::Html::Renderer which has a render method
32
- renderer.render(original)
17
+
18
+ renderer = @context.registers[:renderer]
19
+ return '' unless renderer
20
+
21
+ case input
22
+ when Drop::Base
23
+ renderer.render_drop(input)
24
+ when Array
25
+ input.map { |i| render_element(i) }.join("\n")
26
+ when Hash
27
+ render_hash_data(input, renderer)
28
+ when String
29
+ input
30
+ else
31
+ drop = Drop::DropFactory.create(input)
32
+ drop.is_a?(Drop::Base) ? renderer.render_drop(drop) : drop.to_s
33
+ end
33
34
  end
34
35
 
35
- # Escape HTML entities
36
36
  def escape_html(input)
37
- input.to_s
38
- .gsub(/&/, '&amp;')
39
- .gsub(/</, '&lt;')
40
- .gsub(/>/, '&gt;')
41
- .gsub(/"/, '&quot;')
42
- .gsub(/'/, '&#39;')
37
+ Escape.escape_html(input)
43
38
  end
44
39
 
45
40
  # Escape HTML attribute values
46
41
  def escape_attr(input)
47
- input.to_s
48
- .gsub(/&/, '&amp;')
49
- .gsub(/"/, '&quot;')
50
- .gsub(/</, '&lt;')
51
- .gsub(/>/, '&gt;')
42
+ Escape.escape_attr(input)
43
+ end
44
+
45
+ # JSON-encode with </script protection for inline JS
46
+ def safe_json(input)
47
+ Escape.safe_json(input)
48
+ end
49
+
50
+ private
51
+
52
+ def render_hash_data(data, renderer)
53
+ drop_type = data['drop_type']
54
+ return data.to_s unless drop_type
55
+
56
+ template = renderer.find_template(drop_type)
57
+ return data.to_s unless template
58
+
59
+ template.render(data, registers: { renderer: renderer }).strip
52
60
  end
53
61
  end
54
62
  end
55
63
  end
56
64
 
57
- # Register filters with Liquid (using the non-deprecated API)
65
+ # Register filters with Liquid
58
66
  Liquid::Environment.default.register_filter(Coradoc::Html::TemplateFilters)
@@ -23,6 +23,9 @@ module Coradoc
23
23
  # Default template subdirectory within each template root
24
24
  CORE_MODEL_DIR = 'core_model'
25
25
 
26
+ # Canonical default template directory within the gem
27
+ DEFAULT_TEMPLATE_DIR = Pathname.new(File.join(File.dirname(__FILE__), 'templates', CORE_MODEL_DIR)).freeze
28
+
26
29
  attr_reader :user_dirs, :default_dir
27
30
 
28
31
  # Initialize the locator
@@ -42,12 +45,14 @@ module Coradoc
42
45
  def find(type_name)
43
46
  return @cache[type_name] if @cache.key?(type_name)
44
47
 
45
- # First check user directories
48
+ # First check user directories (core_model/ subdir, then root)
46
49
  @user_dirs.each do |dir|
47
- template_path = dir / CORE_MODEL_DIR / "#{type_name}.liquid"
48
- if template_path.exist?
49
- @cache[type_name] = template_path
50
- return template_path
50
+ %W[#{dir}/#{CORE_MODEL_DIR}/#{type_name}.liquid #{dir}/#{type_name}.liquid].each do |template_path|
51
+ path = Pathname.new(template_path)
52
+ if path.exist?
53
+ @cache[type_name] = path
54
+ return path
55
+ end
51
56
  end
52
57
  end
53
58
 
@@ -78,13 +83,14 @@ module Coradoc
78
83
  def available_templates
79
84
  types = Set.new
80
85
 
81
- # Collect from user directories
86
+ # Collect from user directories (root and core_model/ subdir)
82
87
  @user_dirs.each do |dir|
83
- core_model_path = dir / CORE_MODEL_DIR
84
- next unless core_model_path.exist? && core_model_path.directory?
88
+ [dir, dir / CORE_MODEL_DIR].each do |scan_dir|
89
+ next unless scan_dir.exist? && scan_dir.directory?
85
90
 
86
- core_model_path.glob('*.liquid') do |f|
87
- types.add(f.basename('.liquid').to_s)
91
+ scan_dir.glob('*.liquid') do |f|
92
+ types.add(f.basename('.liquid').to_s)
93
+ end
88
94
  end
89
95
  end
90
96
 
@@ -102,7 +108,7 @@ module Coradoc
102
108
  #
103
109
  # @return [Pathname] Path to default templates
104
110
  def default_template_dir
105
- Pathname.new(File.join(File.dirname(__FILE__), 'templates', CORE_MODEL_DIR))
111
+ DEFAULT_TEMPLATE_DIR
106
112
  end
107
113
 
108
114
  # Clear the template cache (useful when template directories change)
@@ -2,12 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module Html
5
- module Theme
6
- # Autoload Theme components
7
- autoload :Base, 'coradoc/html/theme/base'
8
- autoload :Registry, 'coradoc/html/theme/registry'
9
- autoload :ClassicRenderer, 'coradoc/html/theme/classic_renderer'
10
- autoload :ModernRenderer, 'coradoc/html/theme/modern_renderer'
11
- end
5
+ module Theme; end
12
6
  end
13
7
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ # Resolves CoreModel title attributes to plain-text strings.
6
+ #
7
+ # CoreModel titles can be String, CoreModel::Base (with .text),
8
+ # Array of mixed types, or nil. This utility provides a single
9
+ # resolution path used by TocBuilder, TocSerializer, Renderer,
10
+ # LayoutRenderer, and Drop classes.
11
+ module TitleText
12
+ TEXT_TYPES = [CoreModel::TextContent, CoreModel::Term].freeze
13
+
14
+ module_function
15
+
16
+ def resolve(title)
17
+ case title
18
+ when nil then nil
19
+ when String then title
20
+ when CoreModel::Base then resolve_model(title)
21
+ when Array then title.map { |t| resolve_element(t) }.join
22
+ else title.to_s
23
+ end
24
+ end
25
+
26
+ def escape(title)
27
+ resolved = resolve(title)
28
+ resolved ? Escape.escape_html(resolved) : nil
29
+ end
30
+
31
+ def resolve_model(model)
32
+ if text_type?(model) && model.text
33
+ model.text
34
+ elsif content_type?(model) && model.content
35
+ model.content.to_s
36
+ else
37
+ model.to_s
38
+ end
39
+ end
40
+
41
+ def resolve_element(element)
42
+ case element
43
+ when CoreModel::Base then resolve_model(element)
44
+ else element.to_s
45
+ end
46
+ end
47
+
48
+ def text_type?(model)
49
+ TEXT_TYPES.any? { |t| model.is_a?(t) }
50
+ end
51
+
52
+ def content_type?(model)
53
+ model.is_a?(CoreModel::InlineElement) || model.is_a?(CoreModel::StructuralElement)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ # Builds a CoreModel::Toc from a document's StructuralElement tree.
6
+ #
7
+ # Walks the section hierarchy, assigns section numbers, and returns
8
+ # a Toc model with nested TocEntry children. Section numbers are
9
+ # derived from tree position — single source of truth for both
10
+ # TOC rendering and heading numbering.
11
+ class TocBuilder
12
+ def initialize(max_level: 6, numbered: false, section_number_levels: 3)
13
+ @max_level = max_level
14
+ @numbered = numbered
15
+ @section_number_levels = section_number_levels
16
+ end
17
+
18
+ # Build a TocBuilder from renderer-style options hash.
19
+ #
20
+ # @param options [Hash] options with :section_number_levels, :toc_levels, :section_numbers
21
+ # @return [TocBuilder]
22
+ def self.from_options(options)
23
+ section_number_levels = options[:section_number_levels] || 3
24
+ toc_levels = options[:toc_levels] || 2
25
+ max_level = [toc_levels, section_number_levels].min
26
+ new(max_level: max_level, numbered: options[:section_numbers] == true, section_number_levels: section_number_levels)
27
+ end
28
+
29
+ # Build a Toc model from a document.
30
+ #
31
+ # @param document [CoreModel::StructuralElement] the root document
32
+ # @return [CoreModel::Toc] the built TOC with entries and section numbers
33
+ def build(document)
34
+ entries = []
35
+ counters = [0]
36
+ collect_entries(document.children, entries, 1, counters) if document.children
37
+
38
+ CoreModel::Toc.new(
39
+ entries: entries,
40
+ min_level: 1,
41
+ max_level: @max_level,
42
+ numbered: @numbered
43
+ )
44
+ end
45
+
46
+ # Compute a mapping of section_id => section_number_string.
47
+ # Always computes numbers regardless of the +numbered+ flag,
48
+ # since this is used for heading annotation in the body.
49
+ #
50
+ # @param document [CoreModel::StructuralElement] the root document
51
+ # @return [Hash{String => String}] mapping of section ID to number (e.g., "2.1")
52
+ def section_number_map(document)
53
+ map = {}
54
+ entries = []
55
+ counters = [0]
56
+ collect_entries(document.children, entries, 1, counters, always_number: true) if document.children
57
+ flatten_numbers(entries, map)
58
+ map
59
+ end
60
+
61
+ private
62
+
63
+ def collect_entries(items, entries, level, counters, always_number: false)
64
+ return unless items && level <= @max_level
65
+
66
+ items.each do |item|
67
+ next unless item.is_a?(CoreModel::StructuralElement)
68
+ next unless item.section? || item.header?
69
+
70
+ counters[level] = (counters[level] || 0) + 1
71
+ ((level + 1)..@section_number_levels).each { |i| counters[i] = 0 }
72
+
73
+ use_number = always_number || @numbered
74
+ number = use_number && level <= @section_number_levels ? counters[1..level].join('.') : nil
75
+
76
+ children = []
77
+ collect_entries(item.children, children, level + 1, counters, always_number: always_number) if item.children
78
+
79
+ entries << CoreModel::TocEntry.new(
80
+ id: entry_id(item),
81
+ title: entry_title(item),
82
+ level: level,
83
+ number: number,
84
+ children: children
85
+ )
86
+ end
87
+ end
88
+
89
+ def flatten_numbers(entries, map)
90
+ entries.each do |entry|
91
+ map[entry.id] = entry.number if entry.id && entry.number
92
+ flatten_numbers(entry.children, map) if entry.children
93
+ end
94
+ end
95
+
96
+ def entry_title(section)
97
+ TitleText.resolve(section.title)
98
+ end
99
+
100
+ def entry_id(section)
101
+ section.id
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ # Serializes a document's TOC structure to JSON for inline embedding.
6
+ #
7
+ # Used by the SPA layout to provide client-side navigation data.
8
+ class TocSerializer
9
+ def build_json(document, options)
10
+ return { entries: [], numbered: false } unless document.is_a?(CoreModel::StructuralElement)
11
+
12
+ numbered = options[:section_numbers] == true
13
+ builder = TocBuilder.from_options(options)
14
+ toc = builder.build(document)
15
+ { entries: serialize_entries(toc.entries), numbered: numbered }
16
+ end
17
+
18
+ private
19
+
20
+ def serialize_entries(entries)
21
+ entries.map do |entry|
22
+ {
23
+ id: entry.id,
24
+ title: TitleText.resolve(entry.title),
25
+ number: entry.number,
26
+ level: entry.level,
27
+ children: entry.children.any? ? serialize_entries(entry.children) : []
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,28 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'coradoc/core_model'
4
+
3
5
  module Coradoc
4
6
  module Html
5
7
  module Transform
6
- # Transforms CoreModel models to HTML output
8
+ # Transforms CoreModel to HTML output
7
9
  #
8
- # This transformer converts CoreModel to structures suitable for
9
- # HTML rendering. Note: The HTML converters already support CoreModel
10
- # directly, so this transformer primarily passes through the CoreModel.
10
+ # This transformer converts CoreModel to HTML strings by delegating
11
+ # to the existing theme/renderer pipeline.
11
12
  class FromCoreModel
12
13
  class << self
13
- # Transform a CoreModel to HTML-ready structure
14
+ # Transform a CoreModel to HTML string
14
15
  #
15
- # @param model [Coradoc::CoreModel::Base] CoreModel to transform
16
- # @return [Object] HTML-ready structure
17
- def transform(model)
16
+ # @param model [Coradoc::CoreModel::Base, Array] CoreModel to transform
17
+ # @param options [Hash] Renderer options (e.g., theme)
18
+ # @return [String] HTML output
19
+ def transform(model, options = {})
18
20
  case model
19
21
  when Coradoc::CoreModel::Base
20
- # HTML converters already support CoreModel directly
21
- model
22
+ Html.serialize(model, options)
22
23
  when Array
23
- model.map { |item| transform(item) }
24
+ model.map { |item| transform(item, options) }.join("\n")
24
25
  else
25
- model
26
+ model.to_s
26
27
  end
27
28
  end
28
29
  end
@@ -1,31 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'nokogiri'
3
4
  require 'coradoc/core_model'
4
5
 
5
6
  module Coradoc
6
7
  module Html
7
8
  module Transform
8
- # Transforms HTML input models to CoreModel equivalents
9
+ # Transforms Nokogiri HTML nodes to CoreModel
9
10
  #
10
- # HTML input converters now produce CoreModel directly, so this transformer
11
- # is largely a pass-through that ensures the model is CoreModel.
11
+ # Nokogiri serves as the HTML model layer. This transformer converts
12
+ # Nokogiri::XML::Document or Nokogiri::XML::Node objects into CoreModel
13
+ # by delegating to the existing input converter pipeline.
12
14
  class ToCoreModel
13
15
  class << self
14
- # Transform an HTML input model to CoreModel
16
+ # Transform an HTML model (Nokogiri node) to CoreModel
15
17
  #
16
- # @param model [Object] HTML input model to transform
18
+ # @param model [Nokogiri::XML::Document, Nokogiri::XML::Node, Coradoc::CoreModel::Base]
19
+ # HTML input model to transform
17
20
  # @return [Coradoc::CoreModel::Base] CoreModel equivalent
18
21
  def transform(model)
19
- # HTML input now produces CoreModel directly
20
- transform_direct(model)
21
- end
22
-
23
- private
24
-
25
- def transform_direct(model)
26
22
  case model
27
23
  when Coradoc::CoreModel::Base
28
24
  model
25
+ when Nokogiri::XML::Document, Nokogiri::XML::Node
26
+ ::Coradoc::Input::Html::HtmlConverter.to_core_model(model)
29
27
  when Array
30
28
  model.map { |item| transform(item) }
31
29
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module Html
5
- VERSION = '1.1.7'
5
+ VERSION = '1.1.13'
6
6
  end
7
7
  end