coradoc-html 1.1.7

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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/coradoc/html/base.rb +157 -0
  4. data/lib/coradoc/html/config.rb +467 -0
  5. data/lib/coradoc/html/converter_base.rb +177 -0
  6. data/lib/coradoc/html/converters/admonition.rb +180 -0
  7. data/lib/coradoc/html/converters/attribute.rb +68 -0
  8. data/lib/coradoc/html/converters/attribute_reference.rb +60 -0
  9. data/lib/coradoc/html/converters/audio.rb +165 -0
  10. data/lib/coradoc/html/converters/base.rb +615 -0
  11. data/lib/coradoc/html/converters/bibliography.rb +82 -0
  12. data/lib/coradoc/html/converters/bibliography_entry.rb +108 -0
  13. data/lib/coradoc/html/converters/block_image.rb +72 -0
  14. data/lib/coradoc/html/converters/bold.rb +34 -0
  15. data/lib/coradoc/html/converters/break.rb +32 -0
  16. data/lib/coradoc/html/converters/comment_block.rb +42 -0
  17. data/lib/coradoc/html/converters/comment_line.rb +54 -0
  18. data/lib/coradoc/html/converters/cross_reference.rb +59 -0
  19. data/lib/coradoc/html/converters/document.rb +108 -0
  20. data/lib/coradoc/html/converters/example.rb +114 -0
  21. data/lib/coradoc/html/converters/highlight.rb +34 -0
  22. data/lib/coradoc/html/converters/include.rb +68 -0
  23. data/lib/coradoc/html/converters/inline_image.rb +41 -0
  24. data/lib/coradoc/html/converters/italic.rb +34 -0
  25. data/lib/coradoc/html/converters/line_break.rb +31 -0
  26. data/lib/coradoc/html/converters/link.rb +46 -0
  27. data/lib/coradoc/html/converters/list_item.rb +75 -0
  28. data/lib/coradoc/html/converters/listing.rb +99 -0
  29. data/lib/coradoc/html/converters/literal.rb +102 -0
  30. data/lib/coradoc/html/converters/monospace.rb +34 -0
  31. data/lib/coradoc/html/converters/open.rb +78 -0
  32. data/lib/coradoc/html/converters/ordered.rb +53 -0
  33. data/lib/coradoc/html/converters/paragraph.rb +46 -0
  34. data/lib/coradoc/html/converters/quote.rb +113 -0
  35. data/lib/coradoc/html/converters/reviewer_comment.rb +74 -0
  36. data/lib/coradoc/html/converters/reviewer_note.rb +134 -0
  37. data/lib/coradoc/html/converters/section.rb +90 -0
  38. data/lib/coradoc/html/converters/sidebar.rb +113 -0
  39. data/lib/coradoc/html/converters/source.rb +137 -0
  40. data/lib/coradoc/html/converters/source_code.rb +16 -0
  41. data/lib/coradoc/html/converters/span.rb +61 -0
  42. data/lib/coradoc/html/converters/strikethrough.rb +34 -0
  43. data/lib/coradoc/html/converters/subscript.rb +34 -0
  44. data/lib/coradoc/html/converters/superscript.rb +34 -0
  45. data/lib/coradoc/html/converters/table.rb +85 -0
  46. data/lib/coradoc/html/converters/table_cell.rb +203 -0
  47. data/lib/coradoc/html/converters/table_row.rb +45 -0
  48. data/lib/coradoc/html/converters/template_html_converter.rb +105 -0
  49. data/lib/coradoc/html/converters/term.rb +58 -0
  50. data/lib/coradoc/html/converters/text_element.rb +44 -0
  51. data/lib/coradoc/html/converters/underline.rb +34 -0
  52. data/lib/coradoc/html/converters/unordered.rb +47 -0
  53. data/lib/coradoc/html/converters/verse.rb +105 -0
  54. data/lib/coradoc/html/converters/video.rb +179 -0
  55. data/lib/coradoc/html/element_mapping.rb +210 -0
  56. data/lib/coradoc/html/entity.rb +137 -0
  57. data/lib/coradoc/html/input/cleaner.rb +163 -0
  58. data/lib/coradoc/html/input/config.rb +79 -0
  59. data/lib/coradoc/html/input/converters/a.rb +90 -0
  60. data/lib/coradoc/html/input/converters/aside.rb +23 -0
  61. data/lib/coradoc/html/input/converters/audio.rb +50 -0
  62. data/lib/coradoc/html/input/converters/base.rb +116 -0
  63. data/lib/coradoc/html/input/converters/blockquote.rb +25 -0
  64. data/lib/coradoc/html/input/converters/br.rb +19 -0
  65. data/lib/coradoc/html/input/converters/bypass.rb +83 -0
  66. data/lib/coradoc/html/input/converters/code.rb +25 -0
  67. data/lib/coradoc/html/input/converters/div.rb +25 -0
  68. data/lib/coradoc/html/input/converters/dl.rb +106 -0
  69. data/lib/coradoc/html/input/converters/drop.rb +28 -0
  70. data/lib/coradoc/html/input/converters/em.rb +23 -0
  71. data/lib/coradoc/html/input/converters/figure.rb +58 -0
  72. data/lib/coradoc/html/input/converters/h.rb +76 -0
  73. data/lib/coradoc/html/input/converters/head.rb +30 -0
  74. data/lib/coradoc/html/input/converters/hr.rb +20 -0
  75. data/lib/coradoc/html/input/converters/ignore.rb +22 -0
  76. data/lib/coradoc/html/input/converters/img.rb +110 -0
  77. data/lib/coradoc/html/input/converters/li.rb +35 -0
  78. data/lib/coradoc/html/input/converters/mark.rb +21 -0
  79. data/lib/coradoc/html/input/converters/markup.rb +107 -0
  80. data/lib/coradoc/html/input/converters/math.rb +46 -0
  81. data/lib/coradoc/html/input/converters/ol.rb +46 -0
  82. data/lib/coradoc/html/input/converters/p.rb +81 -0
  83. data/lib/coradoc/html/input/converters/pass_through.rb +19 -0
  84. data/lib/coradoc/html/input/converters/pre.rb +59 -0
  85. data/lib/coradoc/html/input/converters/q.rb +24 -0
  86. data/lib/coradoc/html/input/converters/strong.rb +22 -0
  87. data/lib/coradoc/html/input/converters/sub.rb +40 -0
  88. data/lib/coradoc/html/input/converters/sup.rb +40 -0
  89. data/lib/coradoc/html/input/converters/table.rb +64 -0
  90. data/lib/coradoc/html/input/converters/td.rb +70 -0
  91. data/lib/coradoc/html/input/converters/text.rb +67 -0
  92. data/lib/coradoc/html/input/converters/th.rb +20 -0
  93. data/lib/coradoc/html/input/converters/tr.rb +28 -0
  94. data/lib/coradoc/html/input/converters/video.rb +53 -0
  95. data/lib/coradoc/html/input/converters.rb +122 -0
  96. data/lib/coradoc/html/input/errors.rb +22 -0
  97. data/lib/coradoc/html/input/html_converter.rb +170 -0
  98. data/lib/coradoc/html/input/plugin.rb +169 -0
  99. data/lib/coradoc/html/input/plugins/plateau.rb +229 -0
  100. data/lib/coradoc/html/input/postprocessor.rb +31 -0
  101. data/lib/coradoc/html/input.rb +68 -0
  102. data/lib/coradoc/html/output.rb +95 -0
  103. data/lib/coradoc/html/renderer.rb +409 -0
  104. data/lib/coradoc/html/spa.rb +309 -0
  105. data/lib/coradoc/html/static.rb +293 -0
  106. data/lib/coradoc/html/template_config.rb +151 -0
  107. data/lib/coradoc/html/template_helpers.rb +58 -0
  108. data/lib/coradoc/html/template_locator.rb +114 -0
  109. data/lib/coradoc/html/theme/base.rb +231 -0
  110. data/lib/coradoc/html/theme/classic_renderer.rb +390 -0
  111. data/lib/coradoc/html/theme/modern/components/ui_components.rb +344 -0
  112. data/lib/coradoc/html/theme/modern/css_generator.rb +311 -0
  113. data/lib/coradoc/html/theme/modern/javascript_generator.rb +314 -0
  114. data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +382 -0
  115. data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +164 -0
  116. data/lib/coradoc/html/theme/modern/vue_template_generator.rb +374 -0
  117. data/lib/coradoc/html/theme/modern_renderer.rb +250 -0
  118. data/lib/coradoc/html/theme/registry.rb +153 -0
  119. data/lib/coradoc/html/theme.rb +13 -0
  120. data/lib/coradoc/html/transform/from_core_model.rb +32 -0
  121. data/lib/coradoc/html/transform/to_core_model.rb +39 -0
  122. data/lib/coradoc/html/version.rb +7 -0
  123. data/lib/coradoc/html.rb +255 -0
  124. metadata +264 -0
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Theme
6
+ class ModernRenderer
7
+ # Generate Vue.js application code
8
+ module JavascriptGenerator
9
+ autoload :VueTemplates, "#{__dir__}/vue_template_generator"
10
+
11
+ class << self
12
+ # Generate Vue application
13
+ #
14
+ # @param document_data [Hash] Serialized document data
15
+ # @param config [Hash] Theme configuration
16
+ # @return [String] Vue application JavaScript
17
+ def generate(document_data, config)
18
+ # Get Vue templates
19
+ templates = load_templates
20
+
21
+ # Get enhanced document template
22
+ document_template = UIComponents.enhanced_document_template(config)
23
+
24
+ <<~JS
25
+ const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
26
+
27
+ // Define Vue components
28
+ const components = {
29
+ 'element-paragraph': {
30
+ props: ['data'],
31
+ template: `#{templates[:paragraph]}`
32
+ },
33
+ 'element-admonition': {
34
+ props: ['data'],
35
+ setup(props) {
36
+ const admonitionIcon = (style) => {
37
+ const styles = { note: '📝', tip: '💡', warning: '⚠️', caution: '🔥', important: '❗' };
38
+ return styles[style?.toLowerCase()] || 'ℹ️';
39
+ };
40
+ const admonitionTitle = (style) => style?.charAt(0).toUpperCase() + style?.slice(1) || 'Note';
41
+ return { admonitionIcon, admonitionTitle };
42
+ },
43
+ template: `#{templates[:admonition]}`
44
+ },
45
+ 'element-list': {
46
+ props: ['data'],
47
+ template: `#{templates[:list]}`
48
+ },
49
+ 'element-block': {
50
+ props: ['data'],
51
+ setup(props) {
52
+ const blockContent = (data) => {
53
+ if (!data.content || data.content.length === 0) return '';
54
+ return data.content.map(item => item.content || item.text || '').join('');
55
+ };
56
+ const cellContent = (cell) => {
57
+ if (!cell.content || cell.content.length === 0) return '';
58
+ return cell.content.map(item => item.content || item.text || '').join('');
59
+ };
60
+ return { blockContent, cellContent };
61
+ },
62
+ template: `#{templates[:block]}`
63
+ },
64
+ 'element-table': {
65
+ props: ['data'],
66
+ setup(props) {
67
+ const cellContent = (cell) => {
68
+ if (!cell.content || cell.content.length === 0) return '';
69
+ return cell.content.map(item => item.content || item.text || '').join('');
70
+ };
71
+ return { cellContent };
72
+ },
73
+ template: `#{templates[:table]}`
74
+ },
75
+ 'element-image': {
76
+ props: ['data'],
77
+ template: `#{templates[:image]}`
78
+ },
79
+ 'element-link': {
80
+ props: ['data'],
81
+ template: `#{templates[:link]}`
82
+ },
83
+ 'element-xref': {
84
+ props: ['data'],
85
+ template: `#{templates[:xref]}`
86
+ },
87
+ 'element-text': {
88
+ props: ['data'],
89
+ template: '<span>{{ data.content || data }}</span>'
90
+ },
91
+ 'inline-bold': {
92
+ props: ['data'],
93
+ template: '<strong><template v-for="(item, i) in data.content" :key="i">{{ item.content || item }}</template></strong>'
94
+ },
95
+ 'inline-italic': {
96
+ props: ['data'],
97
+ template: '<em><template v-for="(item, i) in data.content" :key="i">{{ item.content || item }}</template></em>'
98
+ },
99
+ 'inline-monospace': {
100
+ props: ['data'],
101
+ template: '<code class="bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded text-sm"><template v-for="(item, i) in data.content" :key="i">{{ item.content || item }}</template></code>'
102
+ },
103
+ 'inline-text': {
104
+ props: ['data'],
105
+ setup(props) {
106
+ const renderContent = (content) => {
107
+ if (!content) return '';
108
+ if (typeof content === 'string') return content;
109
+ if (Array.isArray(content)) {
110
+ return content.map(item => item.content || item.text || item).join('');
111
+ }
112
+ return content;
113
+ };
114
+ return { renderContent };
115
+ },
116
+ template: '<span>{{ renderContent(data.content) || data.text || data }}</span>'
117
+ },
118
+ 'section-section': {
119
+ props: ['data'],
120
+ template: `#{templates[:section]}`
121
+ },
122
+ };
123
+
124
+ const app = createApp({
125
+ components,
126
+ template: `#{document_template}`,
127
+ setup() {
128
+ // Document data (use docData to avoid shadowing global document)
129
+ const docData = #{JSON.generate(document_data)};
130
+ const config = #{JSON.generate(config)};
131
+
132
+ // UI state
133
+ const isDark = ref(false);
134
+ const showToc = ref(#{config[:toc_sticky]});
135
+ const tocCollapsed = ref(false);
136
+ const activeSection = ref('');
137
+ const showBackToTop = ref(false);
138
+ const scrollProgress = ref(0);
139
+
140
+ // Initialize theme from localStorage or system preference
141
+ onMounted(() => {
142
+ // Theme detection
143
+ const storedTheme = localStorage.getItem('theme');
144
+ if (storedTheme) {
145
+ isDark.value = storedTheme === 'dark';
146
+ } else {
147
+ isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
148
+ }
149
+ applyTheme();
150
+
151
+ // Scroll listeners
152
+ window.addEventListener('scroll', handleScroll);
153
+ window.addEventListener('resize', handleResize);
154
+
155
+ // Initialize intersection observer for active section
156
+ initObserver();
157
+ });
158
+
159
+ onUnmounted(() => {
160
+ window.removeEventListener('scroll', handleScroll);
161
+ window.removeEventListener('resize', handleResize);
162
+ });
163
+
164
+ // Theme toggle
165
+ function toggleTheme() {
166
+ isDark.value = !isDark.value;
167
+ applyTheme();
168
+ localStorage.setItem('theme', isDark.value ? 'dark' : 'light');
169
+ }
170
+
171
+ function applyTheme() {
172
+ if (isDark.value) {
173
+ document.documentElement.classList.add('dark');
174
+ } else {
175
+ document.documentElement.classList.remove('dark');
176
+ }
177
+ }
178
+
179
+ // Scroll handlers
180
+ function handleScroll() {
181
+ // Update scroll progress
182
+ const scrollTop = window.scrollY;
183
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
184
+ scrollProgress.value = (scrollTop / docHeight) * 100;
185
+
186
+ // Show/hide back to top button
187
+ showBackToTop.value = scrollTop > 300;
188
+ }
189
+
190
+ function handleResize() {
191
+ // Auto-collapse TOC on small screens
192
+ if (window.innerWidth < 1024) {
193
+ tocCollapsed.value = true;
194
+ } else {
195
+ tocCollapsed.value = false;
196
+ }
197
+ }
198
+
199
+ function scrollToTop() {
200
+ window.scrollTo({ top: 0, behavior: 'smooth' });
201
+ }
202
+
203
+ function scrollToSection(id) {
204
+ const element = document.getElementById(id);
205
+ if (element) {
206
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
207
+ }
208
+ }
209
+
210
+ // Intersection observer for active section
211
+ function initObserver() {
212
+ const observer = new IntersectionObserver(
213
+ (entries) => {
214
+ entries.forEach((entry) => {
215
+ if (entry.isIntersecting) {
216
+ activeSection.value = entry.target.id;
217
+ }
218
+ });
219
+ },
220
+ {
221
+ rootMargin: '-10% 0px -80% 0px',
222
+ threshold: 0
223
+ }
224
+ );
225
+
226
+ // Observe all sections
227
+ document.querySelectorAll('section[id]').forEach((section) => {
228
+ observer.observe(section);
229
+ });
230
+ }
231
+
232
+ // Copy code to clipboard
233
+ async function copyCode(code, event) {
234
+ try {
235
+ await navigator.clipboard.writeText(code);
236
+ const button = event.target;
237
+ button.textContent = 'Copied!';
238
+ button.classList.add('copied');
239
+ setTimeout(() => {
240
+ button.textContent = 'Copy';
241
+ button.classList.remove('copied');
242
+ }, 2000);
243
+ } catch (err) {
244
+ console.error('Failed to copy code:', err);
245
+ }
246
+ }
247
+
248
+ // Flatten sections for TOC rendering
249
+ function flattenToc(sections, level = 0) {
250
+ const result = [];
251
+ for (const section of sections) {
252
+ result.push({ ...section, level });
253
+ if (section.children && section.children.length > 0) {
254
+ result.push(...flattenToc(section.children, level + 1));
255
+ }
256
+ }
257
+ return result;
258
+ }
259
+
260
+ // Computed properties
261
+ const tocItems = computed(() => flattenToc(docData.toc || []));
262
+
263
+ return {
264
+ document: docData,
265
+ config,
266
+ isDark,
267
+ showToc,
268
+ tocCollapsed,
269
+ activeSection,
270
+ showBackToTop,
271
+ scrollProgress,
272
+ tocItems,
273
+ toggleTheme,
274
+ scrollToTop,
275
+ scrollToSection,
276
+ copyCode,
277
+ };
278
+ },
279
+ });
280
+
281
+ // Register components globally for dynamic component resolution
282
+ // This allows <component :is="..."> to work in nested templates
283
+ Object.keys(components).forEach((name) => {
284
+ app.component(name, components[name]);
285
+ });
286
+
287
+ app.mount('#app');
288
+ JS
289
+ end
290
+
291
+ private
292
+
293
+ # Load Vue component templates
294
+ #
295
+ # @return [Hash] Hash of templates
296
+ def load_templates
297
+ {
298
+ paragraph: VueTemplates.template_for('paragraph'),
299
+ admonition: VueTemplates.template_for('admonition'),
300
+ list: VueTemplates.template_for('list'),
301
+ block: VueTemplates.template_for('block'),
302
+ table: VueTemplates.template_for('table'),
303
+ image: VueTemplates.template_for('image'),
304
+ link: VueTemplates.template_for('link'),
305
+ xref: VueTemplates.template_for('xref'),
306
+ section: VueTemplates.template_for('section')
307
+ }
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Html
5
+ module Theme
6
+ class ModernRenderer
7
+ module Serializers
8
+ # Serialize Coradoc CoreModel to Vue-compatible data structure
9
+ #
10
+ # This module converts the Coradoc CoreModel into a JSON-serializable
11
+ # hash structure that can be consumed by Vue.js components.
12
+ #
13
+ # IMPORTANT: This serializer ONLY handles CoreModel types.
14
+ # All format-specific documents (AsciiDoc, Markdown, etc.) should be
15
+ # transformed to CoreModel before using this serializer.
16
+ module DocumentSerializer
17
+ class << self
18
+ # Serialize document to Vue-compatible format
19
+ #
20
+ # @param document [Coradoc::CoreModel::StructuralElement] Document to serialize
21
+ # @return [Hash] Serialized document data
22
+ # @raise [ArgumentError] if document is not a CoreModel::StructuralElement
23
+ def serialize(document)
24
+ unless document.is_a?(Coradoc::CoreModel::StructuralElement)
25
+ raise ArgumentError,
26
+ "Expected CoreModel::StructuralElement, got #{document.class}. " \
27
+ 'Transform your document to CoreModel first using the appropriate ' \
28
+ 'format transformer (e.g., ToCoreModel for your source format).'
29
+ end
30
+
31
+ serialize_core_model_document(document)
32
+ end
33
+
34
+ # Serialize CoreModel::StructuralElement document
35
+ #
36
+ # @param document [Coradoc::CoreModel::StructuralElement] Document to serialize
37
+ # @return [Hash] Serialized document data
38
+ def serialize_core_model_document(document)
39
+ {
40
+ id: document.id || generate_uid(document),
41
+ type: 'document',
42
+ header: serialize_core_model_header(document),
43
+ attributes: {},
44
+ sections: serialize_core_model_children(document.children),
45
+ toc: build_toc_data_from_core_model(document.children),
46
+ metadata: {
47
+ title: document.title || 'Untitled Document',
48
+ author: nil,
49
+ revision: nil
50
+ }
51
+ }
52
+ end
53
+
54
+ # Serialize CoreModel header (extracted from title)
55
+ #
56
+ # @param document [Coradoc::CoreModel::StructuralElement] Document
57
+ # @return [Hash, nil] Serialized header
58
+ def serialize_core_model_header(document)
59
+ return nil unless document.title
60
+
61
+ {
62
+ id: generate_uid(document),
63
+ type: 'header',
64
+ title: {
65
+ type: 'title',
66
+ text: document.title.to_s,
67
+ level: 1
68
+ },
69
+ author: nil
70
+ }
71
+ end
72
+
73
+ # Serialize CoreModel children
74
+ #
75
+ # @param children [Array, nil] Children to serialize
76
+ # @return [Array] Serialized children
77
+ def serialize_core_model_children(children)
78
+ return [] unless children
79
+
80
+ children.map do |child|
81
+ serialize_core_model_element(child)
82
+ end.compact
83
+ end
84
+
85
+ # Serialize individual CoreModel element
86
+ #
87
+ # @param element [Object] Element to serialize
88
+ # @return [Hash] Serialized element
89
+ def serialize_core_model_element(element)
90
+ case element
91
+ when Coradoc::CoreModel::StructuralElement
92
+ serialize_core_model_section(element)
93
+ when Coradoc::CoreModel::Block
94
+ serialize_core_model_block(element)
95
+ when Coradoc::CoreModel::ListBlock
96
+ serialize_core_model_list(element)
97
+ when Coradoc::CoreModel::Table
98
+ serialize_core_model_table(element)
99
+ when Coradoc::CoreModel::Image
100
+ serialize_core_model_image(element)
101
+ when Coradoc::CoreModel::AnnotationBlock
102
+ serialize_core_model_admonition(element)
103
+ when Coradoc::CoreModel::InlineElement
104
+ serialize_core_model_inline(element)
105
+ else
106
+ serialize_generic_element(element)
107
+ end
108
+ end
109
+
110
+ # Serialize CoreModel section
111
+ #
112
+ # @param section [Coradoc::CoreModel::StructuralElement] Section to serialize
113
+ # @return [Hash] Serialized section
114
+ def serialize_core_model_section(section)
115
+ level = section.level || 1
116
+ {
117
+ id: section.id || generate_uid(section),
118
+ type: 'section',
119
+ title: section.title ? { type: 'title', text: section.title.to_s, level: level } : nil,
120
+ level: level,
121
+ content: [],
122
+ sections: serialize_core_model_children(section.children)
123
+ }
124
+ end
125
+
126
+ # Serialize CoreModel block
127
+ #
128
+ # @param block [Coradoc::CoreModel::Block] Block to serialize
129
+ # @return [Hash] Serialized block
130
+ def serialize_core_model_block(block)
131
+ semantic = block.block_semantic_type&.to_sym
132
+ block_type = case semantic
133
+ when :paragraph then 'paragraph'
134
+ when :source_code then 'source'
135
+ when :quote, :verse then 'quote'
136
+ when :example then 'example'
137
+ else 'block'
138
+ end
139
+
140
+ {
141
+ id: block.id || generate_uid(block),
142
+ type: block_type,
143
+ block_type: block_type,
144
+ title: block.title,
145
+ content: serialize_block_content(block),
146
+ language: block.language
147
+ }
148
+ end
149
+
150
+ # Serialize block content
151
+ #
152
+ # @param block [Coradoc::CoreModel::Block] Block to serialize
153
+ # @return [Array] Serialized content
154
+ def serialize_block_content(block)
155
+ return [] unless block.content
156
+
157
+ case block.content
158
+ when Array
159
+ block.content.map { |el| serialize_core_model_element(el) }.compact
160
+ when Coradoc::CoreModel::InlineElement
161
+ [serialize_core_model_inline(block.content)]
162
+ else
163
+ [{ type: 'text', content: block.content.to_s }]
164
+ end
165
+ end
166
+
167
+ # Serialize CoreModel list
168
+ #
169
+ # @param list [Coradoc::CoreModel::ListBlock] List to serialize
170
+ # @return [Hash] Serialized list
171
+ def serialize_core_model_list(list)
172
+ list_type = case list.marker_type
173
+ when 'ordered', '1' then 'ordered'
174
+ when 'unordered', '*', '-' then 'unordered'
175
+ when 'definition' then 'definition'
176
+ else 'unordered'
177
+ end
178
+
179
+ {
180
+ id: generate_uid(list),
181
+ type: 'list',
182
+ list_type: list_type,
183
+ items: (list.items || []).map do |item|
184
+ serialize_list_item(item)
185
+ end
186
+ }
187
+ end
188
+
189
+ # Serialize list item
190
+ #
191
+ # @param item [Coradoc::CoreModel::ListItem] List item to serialize
192
+ # @return [Hash] Serialized list item
193
+ def serialize_list_item(item)
194
+ content = if item.content
195
+ case item.content
196
+ when Array
197
+ item.content.map { |el| serialize_core_model_element(el) }.compact
198
+ else
199
+ [{ type: 'text', content: item.content.to_s }]
200
+ end
201
+ else
202
+ []
203
+ end
204
+
205
+ {
206
+ id: generate_uid(item),
207
+ type: 'list_item',
208
+ content: content
209
+ }
210
+ end
211
+
212
+ # Serialize CoreModel table
213
+ #
214
+ # @param table [Coradoc::CoreModel::Table] Table to serialize
215
+ # @return [Hash] Serialized table
216
+ def serialize_core_model_table(table)
217
+ {
218
+ id: table.id || generate_uid(table),
219
+ type: 'table',
220
+ title: nil,
221
+ rows: (table.rows || []).map do |row|
222
+ serialize_table_row(row)
223
+ end
224
+ }
225
+ end
226
+
227
+ # Serialize table row
228
+ #
229
+ # @param row [Coradoc::CoreModel::TableRow] Row to serialize
230
+ # @return [Hash] Serialized row
231
+ def serialize_table_row(row)
232
+ {
233
+ type: 'table_row',
234
+ header: row.is_a?(Coradoc::CoreModel::TableRow) && row.header,
235
+ cells: (row.cells || []).map do |cell|
236
+ serialize_table_cell(cell)
237
+ end
238
+ }
239
+ end
240
+
241
+ # Serialize table cell
242
+ #
243
+ # @param cell [Coradoc::CoreModel::TableCell] Cell to serialize
244
+ # @return [Hash] Serialized cell
245
+ def serialize_table_cell(cell)
246
+ content = if cell.content
247
+ case cell.content
248
+ when Array
249
+ cell.content.map { |el| serialize_core_model_element(el) }.compact
250
+ else
251
+ cell.content.to_s
252
+ end
253
+ else
254
+ ''
255
+ end
256
+
257
+ {
258
+ type: 'table_cell',
259
+ content: content
260
+ }
261
+ end
262
+
263
+ # Serialize CoreModel image
264
+ #
265
+ # @param image [Coradoc::CoreModel::Image] Image to serialize
266
+ # @return [Hash] Serialized image
267
+ def serialize_core_model_image(image)
268
+ {
269
+ type: 'image',
270
+ src: image.src,
271
+ alt: image.alt,
272
+ title: nil,
273
+ width: image.width,
274
+ height: image.height,
275
+ inline: image.inline
276
+ }
277
+ end
278
+
279
+ # Serialize CoreModel admonition
280
+ #
281
+ # @param admonition [Coradoc::CoreModel::AnnotationBlock] Admonition to serialize
282
+ # @return [Hash] Serialized admonition
283
+ def serialize_core_model_admonition(admonition)
284
+ {
285
+ id: generate_uid(admonition),
286
+ type: 'admonition',
287
+ style: admonition.annotation_type || :note,
288
+ content: admonition.content ? [{ type: 'text', content: admonition.content.to_s }] : []
289
+ }
290
+ end
291
+
292
+ # Serialize CoreModel inline element
293
+ #
294
+ # @param element [Coradoc::CoreModel::InlineElement] Inline element to serialize
295
+ # @return [Hash] Serialized inline element
296
+ def serialize_core_model_inline(element)
297
+ element_type = case element.inline_type
298
+ when 'bold' then 'bold'
299
+ when 'italic' then 'italic'
300
+ when 'monospace' then 'monospace'
301
+ when 'link' then 'link'
302
+ when 'xref' then 'xref'
303
+ when 'highlight' then 'highlight'
304
+ when 'strikethrough' then 'strikethrough'
305
+ when 'underline' then 'underline'
306
+ when 'subscript' then 'subscript'
307
+ when 'superscript' then 'superscript'
308
+ else 'inline'
309
+ end
310
+
311
+ result = {
312
+ type: element_type,
313
+ content: element.text.to_s
314
+ }
315
+
316
+ # Add link-specific attributes
317
+ result[:href] = element.target if element.inline_type == 'link' && element.target
318
+
319
+ # Add cross-ref specific attributes
320
+ result[:target] = element.target if element.inline_type == 'xref' && element.target
321
+
322
+ result
323
+ end
324
+
325
+ # Build TOC data from CoreModel children
326
+ #
327
+ # @param children [Array] Children to process
328
+ # @param level [Integer] Current level
329
+ # @return [Array] TOC entries
330
+ def build_toc_data_from_core_model(children, level = 1)
331
+ return [] unless children
332
+
333
+ children.each_with_object([]) do |child, result|
334
+ next unless child.is_a?(Coradoc::CoreModel::StructuralElement)
335
+
336
+ entry = {
337
+ id: child.id || generate_uid(child),
338
+ title: child.title.to_s,
339
+ level: level,
340
+ children: build_toc_data_from_core_model(child.children, level + 1)
341
+ }
342
+
343
+ result << entry
344
+ end
345
+ end
346
+
347
+ # Serialize generic element (fallback for unknown types)
348
+ #
349
+ # @param element [Object] Element to serialize
350
+ # @return [Hash] Serialized element
351
+ def serialize_generic_element(element)
352
+ text_content = case element
353
+ when String
354
+ element
355
+ else
356
+ str = element.to_s
357
+ str.include?('#<') ? '' : str
358
+ end
359
+
360
+ {
361
+ type: 'generic',
362
+ class: element.class.name,
363
+ content: text_content
364
+ }
365
+ end
366
+
367
+ private
368
+
369
+ # Generate unique ID for object
370
+ #
371
+ # @param object [Object] Object to generate ID for
372
+ # @return [String] Unique ID
373
+ def generate_uid(object)
374
+ object.object_id.to_s(36)
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end