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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/coradoc/html/base.rb +157 -0
- data/lib/coradoc/html/config.rb +467 -0
- data/lib/coradoc/html/converter_base.rb +177 -0
- data/lib/coradoc/html/converters/admonition.rb +180 -0
- data/lib/coradoc/html/converters/attribute.rb +68 -0
- data/lib/coradoc/html/converters/attribute_reference.rb +60 -0
- data/lib/coradoc/html/converters/audio.rb +165 -0
- data/lib/coradoc/html/converters/base.rb +615 -0
- data/lib/coradoc/html/converters/bibliography.rb +82 -0
- data/lib/coradoc/html/converters/bibliography_entry.rb +108 -0
- data/lib/coradoc/html/converters/block_image.rb +72 -0
- data/lib/coradoc/html/converters/bold.rb +34 -0
- data/lib/coradoc/html/converters/break.rb +32 -0
- data/lib/coradoc/html/converters/comment_block.rb +42 -0
- data/lib/coradoc/html/converters/comment_line.rb +54 -0
- data/lib/coradoc/html/converters/cross_reference.rb +59 -0
- data/lib/coradoc/html/converters/document.rb +108 -0
- data/lib/coradoc/html/converters/example.rb +114 -0
- data/lib/coradoc/html/converters/highlight.rb +34 -0
- data/lib/coradoc/html/converters/include.rb +68 -0
- data/lib/coradoc/html/converters/inline_image.rb +41 -0
- data/lib/coradoc/html/converters/italic.rb +34 -0
- data/lib/coradoc/html/converters/line_break.rb +31 -0
- data/lib/coradoc/html/converters/link.rb +46 -0
- data/lib/coradoc/html/converters/list_item.rb +75 -0
- data/lib/coradoc/html/converters/listing.rb +99 -0
- data/lib/coradoc/html/converters/literal.rb +102 -0
- data/lib/coradoc/html/converters/monospace.rb +34 -0
- data/lib/coradoc/html/converters/open.rb +78 -0
- data/lib/coradoc/html/converters/ordered.rb +53 -0
- data/lib/coradoc/html/converters/paragraph.rb +46 -0
- data/lib/coradoc/html/converters/quote.rb +113 -0
- data/lib/coradoc/html/converters/reviewer_comment.rb +74 -0
- data/lib/coradoc/html/converters/reviewer_note.rb +134 -0
- data/lib/coradoc/html/converters/section.rb +90 -0
- data/lib/coradoc/html/converters/sidebar.rb +113 -0
- data/lib/coradoc/html/converters/source.rb +137 -0
- data/lib/coradoc/html/converters/source_code.rb +16 -0
- data/lib/coradoc/html/converters/span.rb +61 -0
- data/lib/coradoc/html/converters/strikethrough.rb +34 -0
- data/lib/coradoc/html/converters/subscript.rb +34 -0
- data/lib/coradoc/html/converters/superscript.rb +34 -0
- data/lib/coradoc/html/converters/table.rb +85 -0
- data/lib/coradoc/html/converters/table_cell.rb +203 -0
- data/lib/coradoc/html/converters/table_row.rb +45 -0
- data/lib/coradoc/html/converters/template_html_converter.rb +105 -0
- data/lib/coradoc/html/converters/term.rb +58 -0
- data/lib/coradoc/html/converters/text_element.rb +44 -0
- data/lib/coradoc/html/converters/underline.rb +34 -0
- data/lib/coradoc/html/converters/unordered.rb +47 -0
- data/lib/coradoc/html/converters/verse.rb +105 -0
- data/lib/coradoc/html/converters/video.rb +179 -0
- data/lib/coradoc/html/element_mapping.rb +210 -0
- data/lib/coradoc/html/entity.rb +137 -0
- data/lib/coradoc/html/input/cleaner.rb +163 -0
- data/lib/coradoc/html/input/config.rb +79 -0
- data/lib/coradoc/html/input/converters/a.rb +90 -0
- data/lib/coradoc/html/input/converters/aside.rb +23 -0
- data/lib/coradoc/html/input/converters/audio.rb +50 -0
- data/lib/coradoc/html/input/converters/base.rb +116 -0
- data/lib/coradoc/html/input/converters/blockquote.rb +25 -0
- data/lib/coradoc/html/input/converters/br.rb +19 -0
- data/lib/coradoc/html/input/converters/bypass.rb +83 -0
- data/lib/coradoc/html/input/converters/code.rb +25 -0
- data/lib/coradoc/html/input/converters/div.rb +25 -0
- data/lib/coradoc/html/input/converters/dl.rb +106 -0
- data/lib/coradoc/html/input/converters/drop.rb +28 -0
- data/lib/coradoc/html/input/converters/em.rb +23 -0
- data/lib/coradoc/html/input/converters/figure.rb +58 -0
- data/lib/coradoc/html/input/converters/h.rb +76 -0
- data/lib/coradoc/html/input/converters/head.rb +30 -0
- data/lib/coradoc/html/input/converters/hr.rb +20 -0
- data/lib/coradoc/html/input/converters/ignore.rb +22 -0
- data/lib/coradoc/html/input/converters/img.rb +110 -0
- data/lib/coradoc/html/input/converters/li.rb +35 -0
- data/lib/coradoc/html/input/converters/mark.rb +21 -0
- data/lib/coradoc/html/input/converters/markup.rb +107 -0
- data/lib/coradoc/html/input/converters/math.rb +46 -0
- data/lib/coradoc/html/input/converters/ol.rb +46 -0
- data/lib/coradoc/html/input/converters/p.rb +81 -0
- data/lib/coradoc/html/input/converters/pass_through.rb +19 -0
- data/lib/coradoc/html/input/converters/pre.rb +59 -0
- data/lib/coradoc/html/input/converters/q.rb +24 -0
- data/lib/coradoc/html/input/converters/strong.rb +22 -0
- data/lib/coradoc/html/input/converters/sub.rb +40 -0
- data/lib/coradoc/html/input/converters/sup.rb +40 -0
- data/lib/coradoc/html/input/converters/table.rb +64 -0
- data/lib/coradoc/html/input/converters/td.rb +70 -0
- data/lib/coradoc/html/input/converters/text.rb +67 -0
- data/lib/coradoc/html/input/converters/th.rb +20 -0
- data/lib/coradoc/html/input/converters/tr.rb +28 -0
- data/lib/coradoc/html/input/converters/video.rb +53 -0
- data/lib/coradoc/html/input/converters.rb +122 -0
- data/lib/coradoc/html/input/errors.rb +22 -0
- data/lib/coradoc/html/input/html_converter.rb +170 -0
- data/lib/coradoc/html/input/plugin.rb +169 -0
- data/lib/coradoc/html/input/plugins/plateau.rb +229 -0
- data/lib/coradoc/html/input/postprocessor.rb +31 -0
- data/lib/coradoc/html/input.rb +68 -0
- data/lib/coradoc/html/output.rb +95 -0
- data/lib/coradoc/html/renderer.rb +409 -0
- data/lib/coradoc/html/spa.rb +309 -0
- data/lib/coradoc/html/static.rb +293 -0
- data/lib/coradoc/html/template_config.rb +151 -0
- data/lib/coradoc/html/template_helpers.rb +58 -0
- data/lib/coradoc/html/template_locator.rb +114 -0
- data/lib/coradoc/html/theme/base.rb +231 -0
- data/lib/coradoc/html/theme/classic_renderer.rb +390 -0
- data/lib/coradoc/html/theme/modern/components/ui_components.rb +344 -0
- data/lib/coradoc/html/theme/modern/css_generator.rb +311 -0
- data/lib/coradoc/html/theme/modern/javascript_generator.rb +314 -0
- data/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +382 -0
- data/lib/coradoc/html/theme/modern/tailwind_config_builder.rb +164 -0
- data/lib/coradoc/html/theme/modern/vue_template_generator.rb +374 -0
- data/lib/coradoc/html/theme/modern_renderer.rb +250 -0
- data/lib/coradoc/html/theme/registry.rb +153 -0
- data/lib/coradoc/html/theme.rb +13 -0
- data/lib/coradoc/html/transform/from_core_model.rb +32 -0
- data/lib/coradoc/html/transform/to_core_model.rb +39 -0
- data/lib/coradoc/html/version.rb +7 -0
- data/lib/coradoc/html.rb +255 -0
- 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
|