uniword 1.0.10 → 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/lib/uniword/document_factory.rb +1 -0
- data/lib/uniword/docx/package_defaults.rb +1 -0
- data/lib/uniword/docx/reconciler.rb +37 -9
- 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/body.rb +18 -0
- data/lib/uniword/wordprocessingml/document_root.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdd8529445dab8a8008addcef94857f4dc453cea19ece485baaac0ef1c4701f2
|
|
4
|
+
data.tar.gz: cbba0e318b8fde04e96cf0717f82e1a4a827ac4c73e47b8c4af853ba4dbd77be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92dc72fbc93219b74f58bb901aca4d1bda56786c761a04ceb724ffad3417406a13e92f962808613d495f8f68a0955278a9f9a2fcfef29848f6d803f88beab907
|
|
7
|
+
data.tar.gz: 59f335ab79d4cb3b72c80c9b07ffecaf3705285017ab494490c1d6f331fa310205a50ccc2b1f3bb2397c313cb392ee33fa99aec477edaac4cba0b11b4fed3f0f
|
|
@@ -50,6 +50,7 @@ module Uniword
|
|
|
50
50
|
package.chart_parts = document.chart_parts if document.chart_parts
|
|
51
51
|
package.custom_xml_items = document.custom_xml_items if document.custom_xml_items
|
|
52
52
|
package.bibliography_sources = document.bibliography_sources if document.bibliography_sources
|
|
53
|
+
package.embeddings = document.embeddings if document.embeddings
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
# Create minimal content types for a valid DOCX
|
|
@@ -17,6 +17,16 @@ module Uniword
|
|
|
17
17
|
class Reconciler
|
|
18
18
|
CONFIG_DIR = File.join(__dir__, "../../../config")
|
|
19
19
|
|
|
20
|
+
# All w1x/w16x extension namespace prefixes declared by namespace_scope
|
|
21
|
+
# on wordprocessingml parts. These MUST be listed in mc:Ignorable so
|
|
22
|
+
# Word doesn't reject them as unknown extensions.
|
|
23
|
+
IGNORABLE_PREFIXES = %w[
|
|
24
|
+
w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# Extension prefixes used in document.xml (adds wp14 for drawing)
|
|
28
|
+
IGNORABLE_PREFIXES_DOCUMENT = (IGNORABLE_PREFIXES + %w[wp14]).freeze
|
|
29
|
+
|
|
20
30
|
def initialize(package, profile: nil)
|
|
21
31
|
@package = package
|
|
22
32
|
@profile = profile
|
|
@@ -507,11 +517,16 @@ module Uniword
|
|
|
507
517
|
|
|
508
518
|
rsid = generate_rsid
|
|
509
519
|
|
|
520
|
+
# Only inject settings defaults for newly created documents.
|
|
521
|
+
# If settings already has substantial content (zoom + compat + rsids),
|
|
522
|
+
# the document was loaded from a real DOCX and defaults would alter
|
|
523
|
+
# its round-trip fidelity.
|
|
524
|
+
loaded_doc = settings.zoom && settings.compat && settings.rsids
|
|
525
|
+
|
|
510
526
|
settings.zoom ||= Wordprocessingml::Zoom.new(percent: 100)
|
|
511
|
-
settings.do_not_display_page_boundaries ||= Wordprocessingml::DoNotDisplayPageBoundaries.new
|
|
512
527
|
settings.proof_state ||= Wordprocessingml::ProofState.new(
|
|
513
528
|
spelling: "clean", grammar: "clean",
|
|
514
|
-
)
|
|
529
|
+
) unless loaded_doc
|
|
515
530
|
settings.default_tab_stop ||= Wordprocessingml::DefaultTabStop.new(val: "720")
|
|
516
531
|
settings.character_spacing_control ||= Wordprocessingml::CharacterSpacingControl.new(
|
|
517
532
|
val: "doNotCompress",
|
|
@@ -545,7 +560,9 @@ module Uniword
|
|
|
545
560
|
record_fix("R2", "Generated w15:docId in GUID format")
|
|
546
561
|
end
|
|
547
562
|
|
|
548
|
-
settings.mc_ignorable =
|
|
563
|
+
settings.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
564
|
+
IGNORABLE_PREFIXES.join(" "),
|
|
565
|
+
)
|
|
549
566
|
end
|
|
550
567
|
|
|
551
568
|
def reconcile_font_table
|
|
@@ -586,7 +603,9 @@ module Uniword
|
|
|
586
603
|
font_table.fonts << font
|
|
587
604
|
end
|
|
588
605
|
|
|
589
|
-
font_table.mc_ignorable =
|
|
606
|
+
font_table.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
607
|
+
IGNORABLE_PREFIXES.join(" "),
|
|
608
|
+
)
|
|
590
609
|
record_fix("R13",
|
|
591
610
|
"Populated font table with profile fonts and signatures")
|
|
592
611
|
end
|
|
@@ -605,7 +624,9 @@ module Uniword
|
|
|
605
624
|
|
|
606
625
|
ensure_default_styles(styles)
|
|
607
626
|
|
|
608
|
-
styles.mc_ignorable =
|
|
627
|
+
styles.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
628
|
+
IGNORABLE_PREFIXES.join(" "),
|
|
629
|
+
)
|
|
609
630
|
record_fix("R10",
|
|
610
631
|
"Ensured styles have docDefaults, latentStyles, and default styles")
|
|
611
632
|
end
|
|
@@ -614,7 +635,9 @@ module Uniword
|
|
|
614
635
|
return unless profile
|
|
615
636
|
return unless package.numbering
|
|
616
637
|
|
|
617
|
-
package.numbering.mc_ignorable =
|
|
638
|
+
package.numbering.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
639
|
+
IGNORABLE_PREFIXES.join(" "),
|
|
640
|
+
)
|
|
618
641
|
|
|
619
642
|
# Validate instance → definition references
|
|
620
643
|
package.numbering.instances.each do |inst|
|
|
@@ -640,8 +663,11 @@ module Uniword
|
|
|
640
663
|
package.web_settings
|
|
641
664
|
end
|
|
642
665
|
|
|
643
|
-
ws.mc_ignorable =
|
|
644
|
-
|
|
666
|
+
ws.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
667
|
+
IGNORABLE_PREFIXES.join(" "),
|
|
668
|
+
)
|
|
669
|
+
ws.allow_png ||= Wordprocessingml::AllowPng.new
|
|
670
|
+
record_fix("R1", "Set mc:Ignorable and allowPNG on webSettings")
|
|
645
671
|
end
|
|
646
672
|
|
|
647
673
|
def reconcile_app_properties
|
|
@@ -735,7 +761,9 @@ module Uniword
|
|
|
735
761
|
return unless package.document&.body
|
|
736
762
|
|
|
737
763
|
doc = package.document
|
|
738
|
-
doc.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
764
|
+
doc.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
765
|
+
IGNORABLE_PREFIXES_DOCUMENT.join(" "),
|
|
766
|
+
)
|
|
739
767
|
|
|
740
768
|
record_fix("R1", "Added mc:Ignorable to document body")
|
|
741
769
|
record_fix("R12", "Assigned rsid and paraId to paragraphs")
|
|
@@ -697,6 +697,12 @@ module Uniword
|
|
|
697
697
|
</style>
|
|
698
698
|
LSB_STYLE
|
|
699
699
|
|
|
700
|
+
class << self
|
|
701
|
+
# Custom CSS override set by callers (e.g., ISO adapter).
|
|
702
|
+
# When set, #style_block returns this instead of the default.
|
|
703
|
+
attr_accessor :custom_style_block
|
|
704
|
+
end
|
|
705
|
+
|
|
700
706
|
# Build the w:LatentStyles block.
|
|
701
707
|
# @return [String] Static latent styles XML
|
|
702
708
|
def self.latent_styles
|
|
@@ -704,9 +710,10 @@ module Uniword
|
|
|
704
710
|
end
|
|
705
711
|
|
|
706
712
|
# Build the CSS style block.
|
|
707
|
-
#
|
|
713
|
+
# Returns custom CSS if set via custom_style_block=, otherwise default.
|
|
714
|
+
# @return [String] CSS style block
|
|
708
715
|
def self.style_block
|
|
709
|
-
STYLE_BLOCK
|
|
716
|
+
custom_style_block || STYLE_BLOCK
|
|
710
717
|
end
|
|
711
718
|
end
|
|
712
719
|
end
|
|
@@ -18,7 +18,8 @@ module Uniword
|
|
|
18
18
|
# output = Uniword::Infrastructure::MimePackager.new(mhtml_doc).build_mime_content
|
|
19
19
|
#
|
|
20
20
|
class OoxmlToMhtmlConverter
|
|
21
|
-
# Static MsoNormalTable CSS (used in wrap_html_document head)
|
|
21
|
+
# Static MsoNormalTable CSS (used in wrap_html_document head).
|
|
22
|
+
# Only used when MhtmlStyleBuilder does not provide custom CSS.
|
|
22
23
|
MSO_NORMAL_TABLE_STYLE = <<~CSS
|
|
23
24
|
<!--[if gte mso 10]>
|
|
24
25
|
<style>
|
|
@@ -30,21 +31,12 @@ module Uniword
|
|
|
30
31
|
mso-style-noshow:yes;
|
|
31
32
|
mso-style-priority:99;
|
|
32
33
|
mso-style-parent:"";
|
|
33
|
-
mso-padding-alt:
|
|
34
|
-
mso-para-margin
|
|
35
|
-
mso-para-margin-
|
|
36
|
-
mso-para-margin-bottom:8.0pt;
|
|
37
|
-
mso-para-margin-left:0in;
|
|
38
|
-
line-height:115%;
|
|
34
|
+
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
|
|
35
|
+
mso-para-margin:0cm;
|
|
36
|
+
mso-para-margin-bottom:.0001pt;
|
|
39
37
|
mso-pagination:widow-orphan;
|
|
40
|
-
font-size:
|
|
41
|
-
font-family:"
|
|
42
|
-
mso-ascii-font-family:Aptos;
|
|
43
|
-
mso-ascii-theme-font:minor-latin;
|
|
44
|
-
mso-hansi-font-family:Aptos;
|
|
45
|
-
mso-hansi-theme-font:minor-latin;
|
|
46
|
-
mso-font-kerning:1.0pt;
|
|
47
|
-
mso-ligatures:standardcontextual;}
|
|
38
|
+
font-size:10pt;
|
|
39
|
+
font-family:"Cambria",serif;}
|
|
48
40
|
</style>
|
|
49
41
|
<![endif]-->
|
|
50
42
|
CSS
|
|
@@ -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
|
data/lib/uniword/version.rb
CHANGED
|
@@ -75,6 +75,18 @@ module Uniword
|
|
|
75
75
|
h[e.name] += 1
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
needed = false
|
|
79
|
+
needed = true if paragraphs.size > counts["p"]
|
|
80
|
+
needed = true if tables.size > counts["tbl"]
|
|
81
|
+
needed = true if structured_document_tags.size > counts["sdt"]
|
|
82
|
+
needed = true if bookmark_starts.size > counts["bookmarkStart"]
|
|
83
|
+
needed = true if bookmark_ends.size > counts["bookmarkEnd"]
|
|
84
|
+
needed = true if section_properties && counts["sectPr"].zero?
|
|
85
|
+
|
|
86
|
+
return unless needed
|
|
87
|
+
|
|
88
|
+
dup_element_order_if_frozen
|
|
89
|
+
|
|
78
90
|
append_missing_elements("p", paragraphs.size - counts["p"])
|
|
79
91
|
append_missing_elements("tbl", tables.size - counts["tbl"])
|
|
80
92
|
append_missing_elements("sdt",
|
|
@@ -89,6 +101,12 @@ module Uniword
|
|
|
89
101
|
element_order << build_order_element("sectPr")
|
|
90
102
|
end
|
|
91
103
|
|
|
104
|
+
def dup_element_order_if_frozen
|
|
105
|
+
return unless element_order.frozen?
|
|
106
|
+
|
|
107
|
+
self.element_order = element_order.dup
|
|
108
|
+
end
|
|
109
|
+
|
|
92
110
|
def append_missing_elements(name, count)
|
|
93
111
|
count.times { element_order << build_order_element(name) }
|
|
94
112
|
end
|
|
@@ -112,6 +112,8 @@ module Uniword
|
|
|
112
112
|
attr_accessor :chart_parts
|
|
113
113
|
# Bibliography sources for sources.xml
|
|
114
114
|
attr_accessor :bibliography_sources
|
|
115
|
+
# OLE/embedded object binaries (word/embeddings/*)
|
|
116
|
+
attr_accessor :embeddings
|
|
115
117
|
# Round-trip parts (copied from DocxPackage during load)
|
|
116
118
|
attr_accessor :settings, :font_table, :web_settings, :document_rels, :theme_rels,
|
|
117
119
|
:package_rels, :content_types, :custom_properties, :custom_xml_items
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: uniword
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1513,6 +1513,7 @@ files:
|
|
|
1513
1513
|
- lib/uniword/transformation/transformation_rule.rb
|
|
1514
1514
|
- lib/uniword/transformation/transformation_rule_registry.rb
|
|
1515
1515
|
- lib/uniword/transformation/transformer.rb
|
|
1516
|
+
- lib/uniword/transformation/yaml_css_generator.rb
|
|
1516
1517
|
- lib/uniword/validation.rb
|
|
1517
1518
|
- lib/uniword/validation/checkers.rb
|
|
1518
1519
|
- lib/uniword/validation/checkers/external_link_checker.rb
|