uniword 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/themes/office_format_scheme.xml +137 -0
- data/data/themes/office_theme.xml +2 -0
- data/lib/uniword/builder/footnote_builder.rb +4 -2
- data/lib/uniword/builder/run_builder.rb +5 -0
- data/lib/uniword/document_factory.rb +1 -0
- data/lib/uniword/docx/package.rb +18 -0
- data/lib/uniword/docx/package_defaults.rb +1 -0
- data/lib/uniword/docx/package_serialization.rb +38 -0
- data/lib/uniword/docx/reconciler.rb +195 -13
- data/lib/uniword/lazy_loader.rb +1 -1
- data/lib/uniword/properties/boolean_element_factory.rb +8 -15
- data/lib/uniword/schema/schema_loader.rb +1 -1
- data/lib/uniword/transformation/mhtml_style_builder.rb +9 -2
- data/lib/uniword/transformation/ooxml_to_mhtml_converter.rb +7 -15
- data/lib/uniword/transformation/yaml_css_generator.rb +815 -0
- data/lib/uniword/transformation.rb +2 -0
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/attached_template.rb +18 -0
- data/lib/uniword/wordprocessingml/body.rb +34 -34
- data/lib/uniword/wordprocessingml/brk_bin.rb +17 -0
- data/lib/uniword/wordprocessingml/brk_bin_sub.rb +17 -0
- data/lib/uniword/wordprocessingml/character_spacing_control.rb +17 -0
- data/lib/uniword/wordprocessingml/clr_scheme_mapping.rb +39 -0
- data/lib/uniword/wordprocessingml/compat.rb +16 -0
- data/lib/uniword/wordprocessingml/decimal_symbol.rb +17 -0
- data/lib/uniword/wordprocessingml/def_jc.rb +17 -0
- data/lib/uniword/wordprocessingml/default_tab_stop.rb +17 -0
- data/lib/uniword/wordprocessingml/disp_def.rb +14 -0
- data/lib/uniword/wordprocessingml/do_not_display_page_boundaries.rb +14 -0
- data/lib/uniword/wordprocessingml/do_not_include_subdocs_in_stats.rb +17 -0
- data/lib/uniword/wordprocessingml/doc_vars.rb +36 -0
- data/lib/uniword/wordprocessingml/document_root.rb +8 -5
- data/lib/uniword/wordprocessingml/endnote_pr.rb +18 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +18 -0
- data/lib/uniword/wordprocessingml/even_and_odd_headers.rb +17 -0
- data/lib/uniword/wordprocessingml/footnote_pos.rb +17 -0
- data/lib/uniword/wordprocessingml/footnote_pr.rb +20 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +18 -0
- data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +19 -0
- data/lib/uniword/wordprocessingml/hyphenation_zone.rb +20 -0
- data/lib/uniword/wordprocessingml/int_lim.rb +17 -0
- data/lib/uniword/wordprocessingml/l_margin.rb +17 -0
- data/lib/uniword/wordprocessingml/list_separator.rb +17 -0
- data/lib/uniword/wordprocessingml/math_font.rb +17 -0
- data/lib/uniword/wordprocessingml/math_pr.rb +49 -0
- data/lib/uniword/wordprocessingml/mirror_margins.rb +17 -0
- data/lib/uniword/wordprocessingml/nary_lim.rb +17 -0
- data/lib/uniword/wordprocessingml/proof_state.rb +19 -0
- data/lib/uniword/wordprocessingml/r_margin.rb +17 -0
- data/lib/uniword/wordprocessingml/rsid.rb +17 -0
- data/lib/uniword/wordprocessingml/rsid_root.rb +17 -0
- data/lib/uniword/wordprocessingml/rsids.rb +22 -0
- data/lib/uniword/wordprocessingml/settings.rb +45 -511
- data/lib/uniword/wordprocessingml/shape_defaults.rb +21 -0
- data/lib/uniword/wordprocessingml/small_frac.rb +17 -0
- data/lib/uniword/wordprocessingml/style_pane_format_filter.rb +50 -0
- data/lib/uniword/wordprocessingml/style_pane_sort_method.rb +20 -0
- data/lib/uniword/wordprocessingml/table_cell.rb +6 -0
- data/lib/uniword/wordprocessingml/table_row_properties.rb +2 -0
- data/lib/uniword/wordprocessingml/theme_font_lang.rb +19 -0
- data/lib/uniword/wordprocessingml/w14_doc_id.rb +17 -0
- data/lib/uniword/wordprocessingml/w15_chart_tracking_ref_based.rb +14 -0
- data/lib/uniword/wordprocessingml/w15_doc_id.rb +17 -0
- data/lib/uniword/wordprocessingml/wrap_indent.rb +17 -0
- data/lib/uniword/wordprocessingml.rb +42 -56
- data/lib/uniword.rb +12 -5
- metadata +50 -2
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Transformation
|
|
5
|
+
# Generates Word HTML4 CSS from YAML style configuration hashes.
|
|
6
|
+
#
|
|
7
|
+
# Converts OOXML style definitions (paragraph/character properties) to CSS
|
|
8
|
+
# rules suitable for MHTML output. Produces @font-face declarations,
|
|
9
|
+
# paragraph/heading/character style rules, @page definitions, and @list
|
|
10
|
+
# definitions.
|
|
11
|
+
#
|
|
12
|
+
# @example Generate CSS from YAML configs
|
|
13
|
+
# generator = YamlCssGenerator.new(
|
|
14
|
+
# styles: YAML.load_file("styles.yml"),
|
|
15
|
+
# doc_defaults: YAML.load_file("doc_defaults.yml"),
|
|
16
|
+
# numbering: YAML.load_file("numbering.yml")
|
|
17
|
+
# )
|
|
18
|
+
# css = generator.generate
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
class YamlCssGenerator
|
|
22
|
+
# Static @font-face declarations for common ISO fonts.
|
|
23
|
+
FONT_FACE_LIBRARY = {
|
|
24
|
+
"Arial" => <<~CSS,
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: Arial;
|
|
27
|
+
panose-1: 2 11 6 4 2 2 2 2 2 4;
|
|
28
|
+
mso-font-charset: 0;
|
|
29
|
+
mso-generic-font-family: swiss;
|
|
30
|
+
mso-font-pitch: variable;
|
|
31
|
+
mso-font-signature: -536859905 -1073711037 9 0 511 0;
|
|
32
|
+
}
|
|
33
|
+
CSS
|
|
34
|
+
"Courier New" => <<~CSS,
|
|
35
|
+
@font-face {
|
|
36
|
+
font-family: "Courier New";
|
|
37
|
+
panose-1: 2 7 3 9 2 2 5 2 4 4;
|
|
38
|
+
mso-font-charset: 0;
|
|
39
|
+
mso-generic-font-family: roman;
|
|
40
|
+
mso-font-pitch: fixed;
|
|
41
|
+
mso-font-signature: -536859905 -1073711037 9 0 511 0;
|
|
42
|
+
}
|
|
43
|
+
CSS
|
|
44
|
+
"Cambria Math" => <<~CSS,
|
|
45
|
+
@font-face {
|
|
46
|
+
font-family: "Cambria Math";
|
|
47
|
+
panose-1: 2 4 5 3 5 4 6 3 2 4;
|
|
48
|
+
mso-font-charset: 0;
|
|
49
|
+
mso-generic-font-family: roman;
|
|
50
|
+
mso-font-pitch: variable;
|
|
51
|
+
mso-font-signature: -536870145 1107305727 0 0 415 0;
|
|
52
|
+
}
|
|
53
|
+
CSS
|
|
54
|
+
"Calibri" => <<~CSS,
|
|
55
|
+
@font-face {
|
|
56
|
+
font-family: Calibri;
|
|
57
|
+
panose-1: 2 15 5 2 2 2 4 3 2 4;
|
|
58
|
+
mso-font-charset: 0;
|
|
59
|
+
mso-generic-font-family: swiss;
|
|
60
|
+
mso-font-pitch: variable;
|
|
61
|
+
mso-font-signature: -536870145 1073786111 1 0 415 0;
|
|
62
|
+
}
|
|
63
|
+
CSS
|
|
64
|
+
"Cambria" => <<~CSS,
|
|
65
|
+
@font-face {
|
|
66
|
+
font-family: Cambria;
|
|
67
|
+
panose-1: 2 4 5 3 5 4 6 3 2 4;
|
|
68
|
+
mso-font-charset: 0;
|
|
69
|
+
mso-generic-font-family: roman;
|
|
70
|
+
mso-font-pitch: variable;
|
|
71
|
+
mso-font-signature: -536870145 1073743103 0 0 415 0;
|
|
72
|
+
}
|
|
73
|
+
CSS
|
|
74
|
+
"Segoe UI" => <<~CSS,
|
|
75
|
+
@font-face {
|
|
76
|
+
font-family: "Segoe UI";
|
|
77
|
+
panose-1: 2 11 5 2 4 2 4 2 2 3;
|
|
78
|
+
mso-font-charset: 0;
|
|
79
|
+
mso-generic-font-family: swiss;
|
|
80
|
+
mso-font-pitch: variable;
|
|
81
|
+
mso-font-signature: -469750017 -1073683329 9 0 511 0;
|
|
82
|
+
}
|
|
83
|
+
CSS
|
|
84
|
+
"Times New Roman" => <<~CSS,
|
|
85
|
+
@font-face {
|
|
86
|
+
font-family: "Times New Roman";
|
|
87
|
+
panose-1: 2 2 6 3 5 4 5 2 3 4;
|
|
88
|
+
mso-font-charset: 0;
|
|
89
|
+
mso-generic-font-family: roman;
|
|
90
|
+
mso-font-pitch: variable;
|
|
91
|
+
mso-font-signature: -536870145 1073743103 0 0 415 0;
|
|
92
|
+
}
|
|
93
|
+
CSS
|
|
94
|
+
"SimSun" => <<~CSS,
|
|
95
|
+
@font-face {
|
|
96
|
+
font-family: "SimSun";
|
|
97
|
+
panose-1: 2 1 6 0 3 1 1 1 1 1;
|
|
98
|
+
mso-font-charset: 134;
|
|
99
|
+
mso-generic-font-family: auto;
|
|
100
|
+
mso-font-pitch: variable;
|
|
101
|
+
mso-font-signature: 515 135135232 16 0 262144 0;
|
|
102
|
+
}
|
|
103
|
+
CSS
|
|
104
|
+
"SimHei" => <<~CSS,
|
|
105
|
+
@font-face {
|
|
106
|
+
font-family: "SimHei";
|
|
107
|
+
panose-1: 2 1 6 9 6 1 1 1 1 1;
|
|
108
|
+
mso-font-charset: 134;
|
|
109
|
+
mso-generic-font-family: modern;
|
|
110
|
+
mso-font-pitch: fixed;
|
|
111
|
+
mso-font-signature: 515 135135232 16 0 262144 0;
|
|
112
|
+
}
|
|
113
|
+
CSS
|
|
114
|
+
"MS Mincho" => <<~CSS,
|
|
115
|
+
@font-face {
|
|
116
|
+
font-family: "MS Mincho";
|
|
117
|
+
panose-1: 2 2 6 9 4 2 5 8 3 4;
|
|
118
|
+
mso-font-charset: 128;
|
|
119
|
+
mso-generic-font-family: roman;
|
|
120
|
+
mso-font-pitch: fixed;
|
|
121
|
+
mso-font-signature: -536870145 1791491579 134217746 0 131231 0;
|
|
122
|
+
}
|
|
123
|
+
CSS
|
|
124
|
+
"Malgun Gothic" => <<~CSS,
|
|
125
|
+
@font-face {
|
|
126
|
+
font-family: "Malgun Gothic";
|
|
127
|
+
panose-1: 2 11 5 3 2 0 0 2 0 4;
|
|
128
|
+
mso-font-charset: 129;
|
|
129
|
+
mso-generic-font-family: swiss;
|
|
130
|
+
mso-font-pitch: variable;
|
|
131
|
+
mso-font-signature: -1879048145 701988091 18 0 524289 0;
|
|
132
|
+
}
|
|
133
|
+
CSS
|
|
134
|
+
"Cambria serif" => <<~CSS,
|
|
135
|
+
@font-face {
|
|
136
|
+
font-family: "Cambria", serif;
|
|
137
|
+
panose-1: 2 2 6 9 4 2 5 8 3 4;
|
|
138
|
+
mso-font-charset: 128;
|
|
139
|
+
mso-generic-font-family: roman;
|
|
140
|
+
mso-font-pitch: fixed;
|
|
141
|
+
mso-font-signature: -536870145 1791491579 134217746 0 131231 0;
|
|
142
|
+
}
|
|
143
|
+
CSS
|
|
144
|
+
}.freeze
|
|
145
|
+
|
|
146
|
+
# Default font-family declaration based on primary font
|
|
147
|
+
DEFAULT_FONT_FAMILY = '"Cambria", serif'
|
|
148
|
+
DEFAULT_FAREAST_FONT = '"SimSun", serif'
|
|
149
|
+
|
|
150
|
+
# @param styles [Hash] Parsed styles.yml content
|
|
151
|
+
# @param doc_defaults [Hash] Parsed doc_defaults.yml content
|
|
152
|
+
# @param numbering [Hash, nil] Parsed numbering.yml content
|
|
153
|
+
def initialize(styles:, doc_defaults:, numbering: nil)
|
|
154
|
+
style_lib = styles["style_library"] || styles
|
|
155
|
+
@para_styles = style_lib["paragraph_styles"] || {}
|
|
156
|
+
@char_styles = style_lib["character_styles"] || {}
|
|
157
|
+
@doc_defaults = doc_defaults["run_properties"] || {}
|
|
158
|
+
@numbering = numbering
|
|
159
|
+
@defaults = resolve_defaults
|
|
160
|
+
@normal_spacing_after = (@para_styles.dig("Normal", "paragraph_properties", "spacing", "after") || "6.0")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Generate complete CSS string for MHTML output.
|
|
164
|
+
def generate
|
|
165
|
+
parts = []
|
|
166
|
+
parts << "@charset \"UTF-8\";"
|
|
167
|
+
parts << generate_font_faces
|
|
168
|
+
parts << "/* Style Definitions */"
|
|
169
|
+
parts << generate_all_styles
|
|
170
|
+
parts << generate_page_section
|
|
171
|
+
parts << generate_list_section
|
|
172
|
+
parts << generate_mso_normal_table
|
|
173
|
+
parts << generate_list_containers
|
|
174
|
+
parts << generate_ol_ul
|
|
175
|
+
parts.compact.join("\n")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Resolve default run properties from doc_defaults merged with Normal style.
|
|
181
|
+
def resolve_defaults
|
|
182
|
+
normal_run = @para_styles.dig("Normal", "run_properties") || {}
|
|
183
|
+
merged = @doc_defaults.merge(normal_run)
|
|
184
|
+
{
|
|
185
|
+
ascii_font: merged.dig("fonts", "ascii") || "Cambria",
|
|
186
|
+
hansi_font: merged.dig("fonts", "hAnsi") || "Cambria",
|
|
187
|
+
fareast_font: merged.dig("fonts", "eastAsia") || "Calibri",
|
|
188
|
+
bidi_font: merged.dig("fonts", "cs") || "Times New Roman",
|
|
189
|
+
font_size: merged["font_size"] || "11.0",
|
|
190
|
+
lang: merged["lang_val"] || "en-GB",
|
|
191
|
+
fareast_lang: merged["lang_eastasia"] || "en-US",
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Collect all referenced fonts and generate @font-face declarations.
|
|
196
|
+
def generate_font_faces
|
|
197
|
+
fonts = Set.new
|
|
198
|
+
collect_fonts_from_styles(@para_styles, fonts)
|
|
199
|
+
collect_fonts_from_styles(@char_styles, fonts)
|
|
200
|
+
fonts << @defaults[:ascii_font] if @defaults[:ascii_font]
|
|
201
|
+
|
|
202
|
+
faces = []
|
|
203
|
+
fonts.each do |font|
|
|
204
|
+
next if font.nil? || font.empty?
|
|
205
|
+
|
|
206
|
+
face = FONT_FACE_LIBRARY[font]
|
|
207
|
+
if face
|
|
208
|
+
faces << face.strip
|
|
209
|
+
else
|
|
210
|
+
faces << generic_font_face(font)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
"/* Font Definitions */\n#{faces.join("\n")}" unless faces.empty?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def collect_fonts_from_styles(styles, fonts)
|
|
217
|
+
styles.each_value do |style|
|
|
218
|
+
run_props = style["run_properties"] || {}
|
|
219
|
+
font_hash = run_props["fonts"] || {}
|
|
220
|
+
font_hash.each_value { |f| fonts << f if f }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def generic_font_face(font)
|
|
225
|
+
<<~CSS.strip
|
|
226
|
+
@font-face {
|
|
227
|
+
font-family: "#{font}";
|
|
228
|
+
mso-font-charset: 0;
|
|
229
|
+
mso-font-pitch: variable;
|
|
230
|
+
}
|
|
231
|
+
CSS
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Generate all style rules (paragraph, heading, character).
|
|
235
|
+
def generate_all_styles
|
|
236
|
+
parts = []
|
|
237
|
+
|
|
238
|
+
# Normal style
|
|
239
|
+
if @para_styles["Normal"]
|
|
240
|
+
parts << generate_mso_normal(@para_styles["Normal"])
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Headings h1-h6
|
|
244
|
+
(1..6).each do |n|
|
|
245
|
+
key = "Heading#{n}"
|
|
246
|
+
style = @para_styles[key]
|
|
247
|
+
parts << generate_heading(n, style, key) if style
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# TOC styles
|
|
251
|
+
(1..9).each do |n|
|
|
252
|
+
key = "TOC#{n}"
|
|
253
|
+
style = @para_styles[key]
|
|
254
|
+
parts << generate_toc_style(n, style, key) if style
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# ISO-specific paragraph styles
|
|
258
|
+
iso_para_styles = %w[
|
|
259
|
+
ForewordTitle IntroTitle BiblioTitle ANNEX
|
|
260
|
+
a2 a3 a4 a5 a6
|
|
261
|
+
Note Example Code Formula Definition Terms TermNum
|
|
262
|
+
FigureTitle AnnexFigureTitle Tabletitle AnnexTableTitle
|
|
263
|
+
Tablebody BodyText ForewordText Source List ListParagraph
|
|
264
|
+
zzSTDTitle zzContents zzCopyright Header Footer
|
|
265
|
+
NormalWeb BalloonText Caption
|
|
266
|
+
]
|
|
267
|
+
iso_para_styles.each do |name|
|
|
268
|
+
style = @para_styles[name]
|
|
269
|
+
parts << generate_paragraph_style(name, style) if style
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Any remaining paragraph styles not yet generated
|
|
273
|
+
generated = Set.new(%w[Normal Heading1 Heading2 Heading3 Heading4 Heading5 Heading6
|
|
274
|
+
TOC1 TOC2 TOC3 TOC4 TOC5 TOC6 TOC7 TOC8 TOC9] + iso_para_styles)
|
|
275
|
+
@para_styles.each do |name, style|
|
|
276
|
+
next if generated.include?(name)
|
|
277
|
+
|
|
278
|
+
parts << generate_paragraph_style(name, style)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Generic p style
|
|
282
|
+
parts << generate_generic_p
|
|
283
|
+
|
|
284
|
+
# Hyperlink styles
|
|
285
|
+
parts << generate_hyperlink_styles
|
|
286
|
+
|
|
287
|
+
# Character styles
|
|
288
|
+
@char_styles.each do |name, style|
|
|
289
|
+
parts << generate_character_style(name, style)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# MsoChpDefault
|
|
293
|
+
parts << generate_mso_chp_default
|
|
294
|
+
|
|
295
|
+
parts.compact.join("\n\n")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def generate_mso_normal(style)
|
|
299
|
+
props = build_css_properties(style)
|
|
300
|
+
<<~CSS
|
|
301
|
+
p.MsoNormal, li.MsoNormal, div.MsoNormal {
|
|
302
|
+
#{props}
|
|
303
|
+
}
|
|
304
|
+
CSS
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def generate_heading(level, style, _key)
|
|
308
|
+
css_props = build_css_properties(style)
|
|
309
|
+
# Heading styles use h1-h6 tag selectors
|
|
310
|
+
<<~CSS
|
|
311
|
+
h#{level} {
|
|
312
|
+
#{css_props}
|
|
313
|
+
}
|
|
314
|
+
CSS
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def generate_toc_style(level, style, _key)
|
|
318
|
+
css_props = build_css_properties(style)
|
|
319
|
+
<<~CSS
|
|
320
|
+
p.MsoToc#{level}, li.MsoToc#{level}, div.MsoToc#{level} {
|
|
321
|
+
#{css_props}
|
|
322
|
+
}
|
|
323
|
+
CSS
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def generate_paragraph_style(name, style)
|
|
327
|
+
css_props = build_css_properties(style)
|
|
328
|
+
<<~CSS
|
|
329
|
+
p.#{name}, li.#{name}, div.#{name} {
|
|
330
|
+
#{css_props}
|
|
331
|
+
}
|
|
332
|
+
CSS
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def generate_character_style(name, style)
|
|
336
|
+
css_props = build_char_css_properties(style)
|
|
337
|
+
return nil if css_props.strip.empty?
|
|
338
|
+
|
|
339
|
+
<<~CSS
|
|
340
|
+
span.#{name} {
|
|
341
|
+
#{css_props}
|
|
342
|
+
}
|
|
343
|
+
CSS
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Build CSS properties string for a paragraph style.
|
|
347
|
+
def build_css_properties(style)
|
|
348
|
+
lines = []
|
|
349
|
+
para_props = style["paragraph_properties"] || {}
|
|
350
|
+
run_props = style["run_properties"] || {}
|
|
351
|
+
|
|
352
|
+
# mso-style metadata
|
|
353
|
+
lines << " mso-style-unhide: no;"
|
|
354
|
+
if style["quick_format"]
|
|
355
|
+
lines << " mso-style-qformat: yes;"
|
|
356
|
+
end
|
|
357
|
+
if style["name"]
|
|
358
|
+
lines << " mso-style-name: \"#{style['name']}\";"
|
|
359
|
+
end
|
|
360
|
+
if style["based_on"]
|
|
361
|
+
lines << " mso-style-parent: \"#{style['based_on']}\";"
|
|
362
|
+
end
|
|
363
|
+
if style["linked_style"]
|
|
364
|
+
lines << " mso-style-link: \"#{style['linked_style']}\";"
|
|
365
|
+
end
|
|
366
|
+
if style["next_style"]
|
|
367
|
+
lines << " mso-style-next: #{style['next_style']};"
|
|
368
|
+
end
|
|
369
|
+
if style["ui_priority"]
|
|
370
|
+
lines << " mso-style-priority: #{style['ui_priority']};"
|
|
371
|
+
end
|
|
372
|
+
if style["semi_hidden"]
|
|
373
|
+
lines << " mso-style-noshow: yes;"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Margins from spacing
|
|
377
|
+
spacing = para_props["spacing"] || {}
|
|
378
|
+
before = spacing["before"]
|
|
379
|
+
after = spacing["after"]
|
|
380
|
+
if before
|
|
381
|
+
lines << " margin-top: #{pt_value(before)};"
|
|
382
|
+
else
|
|
383
|
+
lines << " margin-top: 0cm;"
|
|
384
|
+
end
|
|
385
|
+
lines << " margin-right: 0cm;"
|
|
386
|
+
if after
|
|
387
|
+
lines << " margin-bottom: #{pt_value(after)};"
|
|
388
|
+
else
|
|
389
|
+
lines << " margin-bottom: #{pt_value(@normal_spacing_after)};"
|
|
390
|
+
end
|
|
391
|
+
lines << " margin-left: 0cm;"
|
|
392
|
+
|
|
393
|
+
# Indent
|
|
394
|
+
indent = para_props["indent"] || {}
|
|
395
|
+
if indent["left"] && indent["left"] != "0.0"
|
|
396
|
+
lines << " margin-left: #{cm_value(indent['left'])};"
|
|
397
|
+
end
|
|
398
|
+
if indent["right"] && indent["right"] != "0.0"
|
|
399
|
+
lines << " margin-right: #{cm_value(indent['right'])};"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Text indent (firstLine or hanging)
|
|
403
|
+
if indent["hanging"] && indent["hanging"] != "0.0"
|
|
404
|
+
lines << " text-indent: -#{cm_value(indent['hanging'])};"
|
|
405
|
+
elsif indent["firstLine"] && indent["firstLine"] != "0.0"
|
|
406
|
+
lines << " text-indent: #{cm_value(indent['firstLine'])};"
|
|
407
|
+
else
|
|
408
|
+
lines << " text-indent: 0cm;" unless indent["hanging"]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Alignment
|
|
412
|
+
alignment = para_props["alignment"]
|
|
413
|
+
align_map = { "both" => "justify", "left" => "left", "center" => "center",
|
|
414
|
+
"right" => "right", "distribute" => "justify" }
|
|
415
|
+
if alignment
|
|
416
|
+
lines << " text-align: #{align_map[alignment] || alignment};"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Line height
|
|
420
|
+
line = spacing["line"]
|
|
421
|
+
if line
|
|
422
|
+
lines << " line-height: #{pt_value(line)};"
|
|
423
|
+
rule = spacing["line_rule"]
|
|
424
|
+
if rule == "exact"
|
|
425
|
+
lines << " mso-line-height-rule: exactly;"
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Pagination
|
|
430
|
+
lines << " mso-pagination: widow-orphan;"
|
|
431
|
+
if para_props["keep_next"]
|
|
432
|
+
lines << " page-break-after: avoid;"
|
|
433
|
+
end
|
|
434
|
+
if para_props["page_break_before"]
|
|
435
|
+
lines << " page-break-before: always;"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Outline level
|
|
439
|
+
outline = para_props["outline_level"]
|
|
440
|
+
if outline
|
|
441
|
+
lines << " mso-outline-level: #{outline.to_i + 1};"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Tab stops
|
|
445
|
+
tabs = para_props["tabs"]
|
|
446
|
+
if tabs && !tabs.empty?
|
|
447
|
+
tab_parts = tabs.map do |tab|
|
|
448
|
+
pos = cm_value(tab["position"])
|
|
449
|
+
case tab["type"]
|
|
450
|
+
when "left" then pos
|
|
451
|
+
when "right" then "right #{pos}"
|
|
452
|
+
when "center" then "center #{pos}"
|
|
453
|
+
when "clear" then "clear #{pos}"
|
|
454
|
+
when "num" then pos
|
|
455
|
+
else pos
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
lines << " tab-stops: #{tab_parts.join(' ')};"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Borders
|
|
462
|
+
borders = para_props["borders"]
|
|
463
|
+
if borders && !borders.empty?
|
|
464
|
+
borders.each do |border|
|
|
465
|
+
side = border["type"]
|
|
466
|
+
val = border["val"]
|
|
467
|
+
sz = border["sz"]
|
|
468
|
+
space = border["space"]
|
|
469
|
+
color = border["color"]
|
|
470
|
+
border_str = "#{val}"
|
|
471
|
+
border_str += " ##{color}" if color
|
|
472
|
+
border_str += " #{pt_value(sz)}" if sz
|
|
473
|
+
lines << " border-#{side}: #{border_str};"
|
|
474
|
+
lines << " mso-border-#{side}-alt: solid ##{color} #{pt_value(sz)};" if color && sz
|
|
475
|
+
if space
|
|
476
|
+
padding_side = side == "top" || side == "bottom" ? "top" : "left"
|
|
477
|
+
lines << " padding: #{pt_value(space)};"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Font size
|
|
483
|
+
font_size = run_props["font_size"]
|
|
484
|
+
if font_size
|
|
485
|
+
lines << " font-size: #{pt_value(font_size)};"
|
|
486
|
+
else
|
|
487
|
+
lines << " font-size: #{pt_value(@defaults[:font_size])};"
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Font family
|
|
491
|
+
fonts = run_props["fonts"] || {}
|
|
492
|
+
ascii = fonts["ascii"] || @defaults[:ascii_font]
|
|
493
|
+
if ascii
|
|
494
|
+
lines << " font-family: \"#{ascii}\", serif;"
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Bold/Italic
|
|
498
|
+
if run_props["bold"]
|
|
499
|
+
lines << " font-weight: bold;"
|
|
500
|
+
end
|
|
501
|
+
if run_props["italic"]
|
|
502
|
+
lines << " font-style: italic;"
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Underline
|
|
506
|
+
if run_props["underline"]
|
|
507
|
+
lines << " text-decoration: underline;"
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Color
|
|
511
|
+
color = run_props["color"]
|
|
512
|
+
if color
|
|
513
|
+
lines << " color: ##{color};"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Font-specific mso properties
|
|
517
|
+
fareast = fonts["eastAsia"]
|
|
518
|
+
if fareast
|
|
519
|
+
lines << " mso-fareast-font-family: \"#{fareast}\", serif;"
|
|
520
|
+
elsif ascii == "Cambria"
|
|
521
|
+
lines << " mso-fareast-font-family: \"SimSun\", serif;"
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
bidi = fonts["cs"]
|
|
525
|
+
if bidi
|
|
526
|
+
lines << " mso-bidi-font-family: \"#{bidi}\", serif;"
|
|
527
|
+
elsif ascii
|
|
528
|
+
lines << " mso-bidi-font-family: \"#{ascii}\", serif;"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Language
|
|
532
|
+
lang = run_props["lang_val"]
|
|
533
|
+
lines << " mso-ansi-language: #{lang || @defaults[:lang]};"
|
|
534
|
+
|
|
535
|
+
fareast_lang = run_props["lang_eastasia"]
|
|
536
|
+
if fareast_lang
|
|
537
|
+
lines << " mso-fareast-language: #{lang_code(fareast_lang)};"
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
lines.join("\n")
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Build CSS properties for character styles.
|
|
544
|
+
def build_char_css_properties(style)
|
|
545
|
+
lines = []
|
|
546
|
+
run_props = style["run_properties"] || {}
|
|
547
|
+
|
|
548
|
+
# mso-style metadata
|
|
549
|
+
lines << " mso-style-name: \"#{style['name']}\";" if style["name"]
|
|
550
|
+
if style["ui_priority"]
|
|
551
|
+
lines << " mso-style-priority: #{style['ui_priority']};"
|
|
552
|
+
end
|
|
553
|
+
if style["semi_hidden"]
|
|
554
|
+
lines << " mso-style-noshow: yes;"
|
|
555
|
+
end
|
|
556
|
+
if style["linked_style"]
|
|
557
|
+
lines << " mso-style-link: \"#{style['linked_style']}\";"
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Font size
|
|
561
|
+
font_size = run_props["font_size"]
|
|
562
|
+
if font_size
|
|
563
|
+
lines << " mso-ansi-font-size: #{pt_value(font_size)};"
|
|
564
|
+
lines << " mso-bidi-font-size: #{pt_value(font_size)};" if run_props["font_size_cs"]
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Font family
|
|
568
|
+
fonts = run_props["fonts"] || {}
|
|
569
|
+
ascii = fonts["ascii"]
|
|
570
|
+
if ascii
|
|
571
|
+
lines << " font-family: \"#{ascii}\", serif;"
|
|
572
|
+
lines << " mso-ascii-font-family: #{ascii};"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
fareast = fonts["eastAsia"]
|
|
576
|
+
lines << " mso-fareast-font-family: \"#{fareast}\", serif;" if fareast
|
|
577
|
+
|
|
578
|
+
hansi = fonts["hAnsi"]
|
|
579
|
+
lines << " mso-hansi-font-family: \"#{hansi}\", serif;" if hansi
|
|
580
|
+
|
|
581
|
+
bidi = fonts["cs"]
|
|
582
|
+
lines << " mso-bidi-font-family: \"#{bidi}\";" if bidi
|
|
583
|
+
|
|
584
|
+
# Bold/Italic
|
|
585
|
+
lines << " font-weight: bold;" if run_props["bold"]
|
|
586
|
+
lines << " mso-bidi-font-weight: normal;" if run_props["bold"]
|
|
587
|
+
lines << " font-style: italic;" if run_props["italic"]
|
|
588
|
+
|
|
589
|
+
# Color
|
|
590
|
+
color = run_props["color"]
|
|
591
|
+
lines << " color: ##{color};" if color
|
|
592
|
+
|
|
593
|
+
# Underline
|
|
594
|
+
lines << " text-decoration: underline;" if run_props["underline"]
|
|
595
|
+
|
|
596
|
+
# Language
|
|
597
|
+
lang = run_props["lang_val"]
|
|
598
|
+
lines << " mso-ansi-language: #{lang || @defaults[:lang]};" if lang || @defaults[:lang]
|
|
599
|
+
|
|
600
|
+
lines.join("\n")
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def generate_generic_p
|
|
604
|
+
<<~CSS
|
|
605
|
+
p {
|
|
606
|
+
mso-style-noshow: yes;
|
|
607
|
+
mso-style-priority: 99;
|
|
608
|
+
mso-margin-top-alt: auto;
|
|
609
|
+
margin-right: 0cm;
|
|
610
|
+
mso-margin-bottom-alt: auto;
|
|
611
|
+
mso-pagination: widow-orphan;
|
|
612
|
+
font-size: #{pt_value(@defaults[:font_size])};
|
|
613
|
+
font-family: "#{@defaults[:ascii_font]}", serif;
|
|
614
|
+
mso-ansi-language: #{@defaults[:lang]};
|
|
615
|
+
}
|
|
616
|
+
CSS
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def generate_hyperlink_styles
|
|
620
|
+
<<~CSS
|
|
621
|
+
a:link, span.MsoHyperlink {
|
|
622
|
+
mso-style-priority: 99;
|
|
623
|
+
mso-style-unhide: no;
|
|
624
|
+
mso-style-parent: "";
|
|
625
|
+
color: blue;
|
|
626
|
+
text-decoration: underline;
|
|
627
|
+
text-underline: single;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
a:visited, span.MsoHyperlinkFollowed {
|
|
631
|
+
mso-style-noshow: yes;
|
|
632
|
+
mso-style-priority: 99;
|
|
633
|
+
color: #954F72;
|
|
634
|
+
text-decoration: underline;
|
|
635
|
+
text-underline: single;
|
|
636
|
+
}
|
|
637
|
+
CSS
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def generate_mso_chp_default
|
|
641
|
+
<<~CSS
|
|
642
|
+
.MsoChpDefault {
|
|
643
|
+
mso-style-type: export-only;
|
|
644
|
+
mso-default-props: yes;
|
|
645
|
+
font-family: "#{@defaults[:ascii_font]}", serif;
|
|
646
|
+
font-size: 10pt;
|
|
647
|
+
mso-ascii-font-family: "#{@defaults[:ascii_font]}", serif;
|
|
648
|
+
mso-fareast-font-family: "SimSun", serif;
|
|
649
|
+
mso-hansi-font-family: "#{@defaults[:ascii_font]}", serif;
|
|
650
|
+
}
|
|
651
|
+
CSS
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def generate_mso_normal_table
|
|
655
|
+
<<~CSS
|
|
656
|
+
table.MsoNormalTable {
|
|
657
|
+
mso-style-name: "Table Normal";
|
|
658
|
+
mso-tstyle-rowband-size: 0;
|
|
659
|
+
mso-tstyle-colband-size: 0;
|
|
660
|
+
mso-style-noshow: yes;
|
|
661
|
+
mso-style-priority: 99;
|
|
662
|
+
mso-style-parent: "";
|
|
663
|
+
mso-padding-alt: 0cm 5.4pt 0cm 5.4pt;
|
|
664
|
+
mso-para-margin: 0cm;
|
|
665
|
+
mso-para-margin-bottom: 0.0001pt;
|
|
666
|
+
mso-pagination: widow-orphan;
|
|
667
|
+
font-size: 10pt;
|
|
668
|
+
font-family: "#{@defaults[:ascii_font]}", serif;
|
|
669
|
+
}
|
|
670
|
+
CSS
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Generate @page section definitions.
|
|
674
|
+
def generate_page_section
|
|
675
|
+
<<~CSS
|
|
676
|
+
/* Page Definitions */
|
|
677
|
+
@page {
|
|
678
|
+
mso-mirror-margins: yes;
|
|
679
|
+
mso-footnote-separator: url(cid:header.html) fs;
|
|
680
|
+
mso-footnote-continuation-separator: url(cid:header.html) fcs;
|
|
681
|
+
mso-endnote-separator: url(cid:header.html) es;
|
|
682
|
+
mso-endnote-continuation-separator: url(cid:header.html) ecs;
|
|
683
|
+
mso-facing-pages: yes;
|
|
684
|
+
}
|
|
685
|
+
@page WordSection1 {
|
|
686
|
+
size: 595.3pt 841.9pt;
|
|
687
|
+
margin: 39.7pt 36.85pt 14.2pt 42.55pt;
|
|
688
|
+
mso-header-margin: 35.45pt;
|
|
689
|
+
mso-footer-margin: 0cm;
|
|
690
|
+
mso-gutter-margin: 1cm;
|
|
691
|
+
mso-even-header: url(cid:header.html) eha;
|
|
692
|
+
mso-even-footer: url(cid:header.html) efa;
|
|
693
|
+
mso-footer: url(cid:header.html) fa;
|
|
694
|
+
mso-paper-source: 0;
|
|
695
|
+
}
|
|
696
|
+
div.WordSection1 {
|
|
697
|
+
page: WordSection1;
|
|
698
|
+
}
|
|
699
|
+
CSS
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Generate @list definitions from numbering.yml.
|
|
703
|
+
def generate_list_section
|
|
704
|
+
return "" unless @numbering
|
|
705
|
+
|
|
706
|
+
definitions = @numbering["definitions"] || []
|
|
707
|
+
instances = @numbering["instances"] || []
|
|
708
|
+
|
|
709
|
+
return "" if definitions.empty?
|
|
710
|
+
|
|
711
|
+
parts = []
|
|
712
|
+
definitions.each_with_index do |defn, idx|
|
|
713
|
+
parts << generate_list_definition(idx, defn)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
parts.join("\n")
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def generate_list_definition(idx, defn)
|
|
720
|
+
abstract_id = defn["abstract_num_id"]
|
|
721
|
+
levels = defn["levels"] || []
|
|
722
|
+
parts = ["@list l#{idx} {"]
|
|
723
|
+
|
|
724
|
+
# Build level CSS
|
|
725
|
+
levels.each do |level|
|
|
726
|
+
level_num = level["ilvl"].to_i
|
|
727
|
+
parts << " @list l#{idx}:level#{level_num + 1} {"
|
|
728
|
+
|
|
729
|
+
fmt = level["format"]
|
|
730
|
+
if fmt
|
|
731
|
+
fmt_map = {
|
|
732
|
+
"decimal" => nil, "bullet" => "mso-level-number-format: bullet;",
|
|
733
|
+
"upperLetter" => "mso-level-number-format: alpha-upper;",
|
|
734
|
+
"lowerLetter" => "mso-level-number-format: alpha-lower;",
|
|
735
|
+
"upperRoman" => "mso-level-number-format: roman-upper;",
|
|
736
|
+
"lowerRoman" => "mso-level-number-format: roman-lower;",
|
|
737
|
+
}
|
|
738
|
+
parts << " #{fmt_map[fmt]}" if fmt_map[fmt]
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
text = level["text"]
|
|
742
|
+
parts << " mso-level-text: \"#{text}\";" if text
|
|
743
|
+
|
|
744
|
+
para_props = level["paragraph_properties"] || {}
|
|
745
|
+
indent = para_props["indent"] || {}
|
|
746
|
+
left = indent["left"]
|
|
747
|
+
hanging = indent["hanging"]
|
|
748
|
+
if left
|
|
749
|
+
parts << " margin-left: #{cm_value(left)};"
|
|
750
|
+
end
|
|
751
|
+
if hanging
|
|
752
|
+
parts << " text-indent: -#{cm_value(hanging)};"
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
parts << " }"
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
parts << "}"
|
|
759
|
+
parts.join("\n")
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def generate_list_containers
|
|
763
|
+
lines = (1..9).map do |n|
|
|
764
|
+
<<~CSS
|
|
765
|
+
div.ListContLevel#{n} {
|
|
766
|
+
mso-style-priority: 34;
|
|
767
|
+
margin-left: #{n * 18}pt;
|
|
768
|
+
margin-right: 0cm;
|
|
769
|
+
}
|
|
770
|
+
CSS
|
|
771
|
+
end
|
|
772
|
+
lines.join("\n")
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def generate_ol_ul
|
|
776
|
+
<<~CSS
|
|
777
|
+
ol {
|
|
778
|
+
margin-bottom: 0cm;
|
|
779
|
+
margin-left: 18pt;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
ul {
|
|
783
|
+
margin-bottom: 0cm;
|
|
784
|
+
margin-left: 18pt;
|
|
785
|
+
}
|
|
786
|
+
CSS
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Format a value as points (e.g., "11.0" → "11pt")
|
|
790
|
+
def pt_value(val)
|
|
791
|
+
return "0pt" unless val
|
|
792
|
+
v = val.to_f
|
|
793
|
+
v == v.to_i ? "#{v.to_i}pt" : "#{v}pt"
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Format a value as centimeters (e.g., "0.71" → "0.71cm")
|
|
797
|
+
def cm_value(val)
|
|
798
|
+
return "0cm" unless val
|
|
799
|
+
v = val.to_f
|
|
800
|
+
v == v.to_i ? "#{v.to_i}cm" : "#{v}cm"
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Convert language code: "ja-JP" → "JA", "en-GB" stays "EN-GB"
|
|
804
|
+
def lang_code(lang)
|
|
805
|
+
return lang unless lang
|
|
806
|
+
|
|
807
|
+
case lang
|
|
808
|
+
when "ja-JP" then "JA"
|
|
809
|
+
when "zh-CN" then "ZH-CN"
|
|
810
|
+
else lang.upcase
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
end
|