Skip to content

Commit 69ec874

Browse files
authored
Merge pull request #68 from dmpotter44/numbering
Add support for the "numbering" part
2 parents 7d10f89 + d79915c commit 69ec874

18 files changed

Lines changed: 468 additions & 0 deletions

examples/numbering

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env ruby
2+
3+
# This example shows how to use the "numbering" system to describe how a list
4+
# should be numbered within a document.
5+
6+
# require "rails" # workaround: openxml-package uses `extract_options!`
7+
$:.push Dir.pwd + "/lib"
8+
require "openxml/docx"
9+
10+
package = OpenXml::Docx::Package.new
11+
12+
include OpenXml::Docx::Elements
13+
14+
# Each list item is a paragraph. A helper function to create paragraphs in the
15+
# final document:
16+
def create_paragraph(content)
17+
text = Text.new(content)
18+
run = Run.new
19+
run << text
20+
paragraph = Paragraph.new
21+
paragraph << run
22+
paragraph
23+
end
24+
25+
# First up, create a style for the list paragraph
26+
list_style = OpenXml::Docx::Style.new :paragraph
27+
list_style.id = 'ListParagraph'
28+
list_style.style_name = 'List Paragraph'
29+
list_style.paragraph.indentation.left = 720
30+
list_style.paragraph.contextual_spacing = true
31+
32+
package.styles << list_style
33+
34+
# Each list needs a numbering within the numbering part of the document.
35+
36+
# Create an abstract numbering that describes a bulleted list:
37+
abstract_numbering = AbstractNumbering.new(0)
38+
39+
# Each numbering can have multiple levels. Define the first level as a bulleted list:
40+
level_0 = Level.new
41+
level_0.level = 0
42+
level_0.start = 1
43+
level_0.number_format = :bullet
44+
# This is the default bullet Word uses
45+
level_0.level_text = "\u00B7".encode("UTF-8")
46+
level_0.alignment = :left
47+
level_0.character_style.font.ascii = "Symbol"
48+
level_0.character_style.font.high_ansi = "Symbol"
49+
level_0.character_style.font.hint = :default
50+
level_0.paragraph_style.indentation.left = 720
51+
level_0.paragraph_style.indentation.hanging = 360
52+
abstract_numbering << level_0
53+
54+
package.numbering << abstract_numbering
55+
56+
package.document << create_paragraph("Example of adding a list to a document")
57+
58+
list_item = create_paragraph("First list item")
59+
list_item.paragraph_style = 'ListParagraph'
60+
# Say that this list item belongs to our first abstract numbering:
61+
list_item.numbering.level = 0
62+
list_item.numbering.id = 1
63+
# This ID is NOT the numbering ID we created above. Instead, it is a concrete
64+
# numbering that defines an instance of a list in the document. To link it to
65+
# our existing abstract numbering, we need to create a concrete numbering
66+
# instance:
67+
68+
numbering = Numbering.new(1)
69+
# Setting the abstract numbering ID here links it to a given abstract numbering
70+
numbering.abstract_numbering_id = 0
71+
72+
# And add this number to the numbering
73+
package.numbering << numbering
74+
75+
# All that allows us to finally add the item to the document.
76+
77+
package.document << list_item
78+
79+
list_item = create_paragraph("Second list item")
80+
list_item.paragraph_style = 'ListParagraph'
81+
82+
# The second list item is simpler: we can reuse the numbering we used above.
83+
list_item.numbering.level = 0
84+
list_item.numbering.id = 1
85+
86+
package.document << list_item
87+
88+
package.document << create_paragraph("Outline Example")
89+
90+
# Lists with numbers are a bit more complicated. The following creates an
91+
# "outline" list:
92+
93+
abstract_numbering = AbstractNumbering.new(1)
94+
95+
[:upperRoman, :upperLetter, :decimal, :lowerLetter, :lowerRoman, :decimal, :lowerLetter].each.with_index do |number_format, index|
96+
level = Level.new
97+
level.level = index
98+
level.start = 1
99+
level.number_format = number_format
100+
# Level text replacement tokens are always 1-based, while the levels
101+
# themselves are 0-based
102+
level.level_text = index < 4 ? "%#{index+1}." : "(%#{index+1})"
103+
level.alignment = :left
104+
level.paragraph_style.indentation.left = (360 * (index+1))
105+
level.paragraph_style.indentation.hanging = 360
106+
abstract_numbering << level
107+
end
108+
109+
package.numbering << abstract_numbering
110+
111+
numbering = Numbering.new(2)
112+
numbering.abstract_numbering_id = 1
113+
114+
package.numbering << numbering
115+
116+
(0..6).each do |level|
117+
list_item = create_paragraph("Level #{level+1}")
118+
list_item.paragraph_style = 'ListParagraph'
119+
list_item.numbering.level = level
120+
list_item.numbering.id = 2
121+
package.document << list_item
122+
end
123+
124+
filename = "numbering_example.docx"
125+
package.save File.expand_path("#{filename}")

lib/openxml/docx.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ module Docx
77
REL_FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer".freeze
88
REL_FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable".freeze
99
REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font".freeze
10+
REL_NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering".freeze
1011
REL_IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image".freeze
1112

1213
TYPE_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml".freeze
1314
TYPE_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml".freeze
1415
TYPE_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml".freeze
1516
TYPE_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml".freeze
1617
TYPE_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml".freeze
18+
TYPE_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml".freeze
1719
TYPE_XML = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml".freeze
1820
TYPE_OBSCURED_FONT = "application/vnd.openxmlformats-officedocument.obfuscatedFont".freeze
1921
TYPE_IMAGE = {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module OpenXml
2+
module Docx
3+
module Elements
4+
class AbstractNumbering < OpenXml::Docx::Element
5+
include HasChildren, HasProperties
6+
tag :abstractNum
7+
8+
def initialize(id)
9+
super()
10+
self.id = id
11+
end
12+
# TODO: child levels is limited to a max of 9
13+
14+
with_namespace :w do
15+
attribute :id, expects: :integer, displays_as: :abstractNumId#, required: true
16+
end
17+
18+
# value_property :nsid - this is a UI property and likely not worth implementing
19+
value_property :multi_level_type
20+
# value_property :tmpl - this is a UI property and likely not worth implementing
21+
# value_property :name
22+
# value_property :style_link
23+
# value_property :num_style_link
24+
25+
def property_xml(xml)
26+
props = properties.keys.map(&method(:send)).compact
27+
return if props.none?(&:render?)
28+
29+
props.each { |prop| prop.to_xml(xml) }
30+
end
31+
end
32+
end
33+
end
34+
end

lib/openxml/docx/elements/level.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module OpenXml
2+
module Docx
3+
module Elements
4+
class Level < OpenXml::Docx::Element
5+
include HasChildren, HasProperties
6+
tag :lvl
7+
8+
with_namespace :w do
9+
attribute :level, expects: :integer, displays_as: :ilvl # required
10+
# tplc is an entirely opaque "Word template code" and is
11+
# "application-specific" according to the spec
12+
attribute :template_code, expects: :long_hex_number, displays_as: :tplc
13+
attribute :tentative, expects: :boolean
14+
end
15+
16+
value_property :start
17+
value_property :number_format
18+
value_property :level_restart
19+
value_property :associated_paragraph_style, as: :paragraph_style
20+
value_property :legal_numbering
21+
value_property :suffix
22+
value_property :level_text
23+
# TODO: Add pic_bullet support (this refers to an element that isn't
24+
# implemented in the Numbering part)
25+
# value_property :lvl_pic_bullet_id
26+
value_property :alignment, as: :level_alignment
27+
28+
def paragraph_style
29+
@paragraph_style ||= Paragraph.new
30+
end
31+
32+
def character_style
33+
@character_style ||= Run.new
34+
end
35+
36+
def property_xml(xml)
37+
props = properties.keys.map(&method(:send)).compact
38+
return if props.none?(&:render?)
39+
40+
props.each { |prop| prop.to_xml(xml) }
41+
end
42+
43+
def to_xml(xml)
44+
xml["w"].public_send(tag, xml_attributes) {
45+
property_xml(xml)
46+
@paragraph_style.property_xml(xml) unless @paragraph_style.nil?
47+
@character_style.property_xml(xml) unless @character_style.nil?
48+
}
49+
end
50+
end
51+
end
52+
end
53+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module OpenXml
2+
module Docx
3+
module Elements
4+
class LevelOverride < OpenXml::Docx::Element
5+
include HasChildren, HasProperties
6+
tag :lvlOverride
7+
8+
with_namespace :w do
9+
attribute :level, expects: :integer, displays_as: :ilvl # required
10+
end
11+
12+
value_property :start_override
13+
14+
def override
15+
@override ||= Level.new
16+
end
17+
18+
def to_xml(xml)
19+
xml["w"].public_send(tag, xml_attributes) {
20+
start_override.to_xml(xml)
21+
@override.to_xml(xml) unless @override.nil?
22+
}
23+
end
24+
end
25+
end
26+
end
27+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module OpenXml
2+
module Docx
3+
module Elements
4+
class Numbering < OpenXml::Docx::Element
5+
include HasChildren, HasProperties
6+
tag :num
7+
8+
def initialize(id)
9+
super()
10+
self.id = id
11+
end
12+
# TODO: child lvlOverride is limited to 9
13+
14+
with_namespace :w do
15+
attribute :id, expects: :integer, displays_as: :numId#, required: true
16+
end
17+
18+
value_property :abstract_numbering_id
19+
20+
def property_xml(xml)
21+
props = properties.keys.map(&method(:send)).compact
22+
return if props.none?(&:render?)
23+
24+
props.each { |prop| prop.to_xml(xml) }
25+
end
26+
end
27+
end
28+
end
29+
end

lib/openxml/docx/package.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Package < OpenXml::Package
1010
:footers,
1111
:styles,
1212
:fonts,
13+
:numbering,
1314
:image_names
1415

1516
content_types do
@@ -23,6 +24,7 @@ class Package < OpenXml::Package
2324
override "/word/styles.xml", TYPE_STYLES
2425
override "/word/settings.xml", TYPE_SETTINGS
2526
override "/word/fontTable.xml", TYPE_FONT_TABLE
27+
override "/word/numbering.xml", TYPE_NUMBERING
2628
end
2729

2830
def initialize
@@ -32,6 +34,7 @@ def initialize
3234
@settings = OpenXml::Docx::Parts::Settings.new
3335
@styles = OpenXml::Docx::Parts::Styles.new
3436
@fonts = OpenXml::Docx::Parts::Fonts.new
37+
@numbering = OpenXml::Docx::Parts::Numbering.new
3538
@document = OpenXml::Docx::Parts::Document.new
3639
@headers = []
3740
@footers = []
@@ -40,13 +43,15 @@ def initialize
4043
document.relationships.add_relationship REL_STYLES, "styles.xml"
4144
document.relationships.add_relationship REL_SETTINGS, "settings.xml"
4245
document.relationships.add_relationship REL_FONT_TABLE, "fontTable.xml"
46+
document.relationships.add_relationship REL_NUMBERING, "numbering.xml"
4347

4448
add_part "word/_rels/document.xml.rels", document.relationships
4549
add_part "word/_rels/fontTable.xml.rels", fonts.relationships
4650
add_part "word/document.xml", document
4751
add_part "word/settings.xml", settings
4852
add_part "word/styles.xml", styles
4953
add_part "word/fontTable.xml", fonts
54+
add_part "word/numbering.xml", numbering
5055
end
5156

5257
def embed_truetype_font(path: nil, name: nil)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module OpenXml
2+
module Docx
3+
module Parts
4+
class Numbering < OpenXml::Part
5+
include RootNamespaces
6+
7+
attr_reader :abstractNumbers, :numbers
8+
9+
use_namespaces :w
10+
11+
def initialize
12+
@abstractNumbers = []
13+
@numbers = []
14+
@relationships = OpenXml::Parts::Rels.new
15+
end
16+
17+
def <<(child)
18+
if child.is_a?(OpenXml::Docx::Elements::AbstractNumbering)
19+
abstractNumbers << child
20+
elsif child.is_a?(OpenXml::Docx::Elements::Numbering)
21+
numbers << child
22+
end
23+
end
24+
25+
def count
26+
abstractNums.count
27+
end
28+
29+
def to_xml
30+
build_standalone_xml do |xml|
31+
xml.numbering(root_namespaces) {
32+
xml.parent.namespace = :w
33+
abstractNumbers.each { |num| num.to_xml(xml) }
34+
numbers.each { |number| number.to_xml(xml) }
35+
}
36+
end
37+
end
38+
39+
end
40+
end
41+
end
42+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module OpenXml
2+
module Docx
3+
module Properties
4+
class AbstractNumberingId < IntegerProperty
5+
tag :abstractNumId
6+
7+
end
8+
end
9+
end
10+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module OpenXml
2+
module Docx
3+
module Properties
4+
class LegalNumbering < OnOffProperty
5+
tag :isLgl
6+
7+
end
8+
end
9+
end
10+
end

0 commit comments

Comments
 (0)