docx-cloner 0.0.1 → 0.1.0

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.
Binary file
@@ -0,0 +1,52 @@
1
+ <w:p w14:paraId="4E9418BB" w14:textId="23E8C963" w:rsidR="00342A6D" w:rsidRDefault="00342A6D" w:rsidP="006A0A53">
2
+ <w:pPr>
3
+ <w:pStyle w:val="ab"/>
4
+ <w:spacing w:after="120"/>
5
+ <w:rPr>
6
+ <w:rFonts w:hint="eastAsia"/>
7
+ <w:lang w:eastAsia="zh-CN"/>
8
+ </w:rPr>
9
+ </w:pPr>
10
+ <w:r>
11
+ <w:rPr>
12
+ <w:rFonts w:hint="eastAsia"/>
13
+ <w:lang w:eastAsia="zh-CN"/>
14
+ </w:rPr>
15
+ <w:t>这是一个单词</w:t>
16
+ </w:r>
17
+ <w:r>
18
+ <w:rPr>
19
+ <w:rFonts w:hint="eastAsia"/>
20
+ <w:lang w:eastAsia="zh-CN"/>
21
+ </w:rPr>
22
+ <w:t xml:space="preserve"> </w:t>
23
+ </w:r>
24
+ <w:r w:rsidR="000F595B">
25
+ <w:rPr>
26
+ <w:rFonts w:hint="eastAsia"/>
27
+ <w:lang w:eastAsia="zh-CN"/>
28
+ </w:rPr>
29
+ <w:t>{</w:t>
30
+ </w:r>
31
+ <w:r>
32
+ <w:rPr>
33
+ <w:rFonts w:hint="eastAsia"/>
34
+ <w:lang w:eastAsia="zh-CN"/>
35
+ </w:rPr>
36
+ <w:t>n</w:t>
37
+ </w:r>
38
+ <w:r w:rsidR="000F595B">
39
+ <w:rPr>
40
+ <w:rFonts w:hint="eastAsia"/>
41
+ <w:lang w:eastAsia="zh-CN"/>
42
+ </w:rPr>
43
+ <w:t>ame}</w:t>
44
+ </w:r>
45
+ <w:r>
46
+ <w:rPr>
47
+ <w:rFonts w:hint="eastAsia"/>
48
+ <w:lang w:eastAsia="zh-CN"/>
49
+ </w:rPr>
50
+ <w:t>测试</w:t>
51
+ </w:r>
52
+ </w:p>
@@ -0,0 +1,46 @@
1
+ #language: zh-CN
2
+
3
+ 功能: 读Docx内标签定义
4
+ 这里要确认标签读取的正确性,然后再进入替换阶段
5
+ 1、主要解决的问题包括:将docx文件拆包、找到对应的文件位置
6
+ 2、xml标记可能是散开的,例如"{name}"在docx文件内部表示中,"{"、"name"、"}"是各自独立的xml标记
7
+ 3、替换逻辑,希望使用DSL在程序中指定,因此不应该限定到底使用"{name}"还是"$name$"做标签标识
8
+
9
+ 背景: 可读的示例文件列举
10
+ 假如"docx-examples"示例文件夹中存在一个"read-single-tags.docx"的文件
11
+
12
+ 场景大纲: 简单地读取词语替换标签
13
+ 这是最简单的情形,例如将标签{name},替换为真正的姓名。
14
+
15
+ 那么程序应该能读到"<tagname>"这个标签词
16
+
17
+ 例子: 读取标签的例子
18
+ "{}"可作为默认的正则表达式设计,在DSL中无需指定
19
+ 程序应该支持中文(以及其它UTF8字符)
20
+
21
+ | tagname |
22
+ | name |
23
+ | {name} |
24
+ | {Name} |
25
+ | {NAME} |
26
+ | {{名字}} |
27
+ | $名字$ |
28
+
29
+ @wip
30
+ 场景大纲: 读取表格行替换标签
31
+ 这通常是在表格上追加行所使用的
32
+
33
+ 那么程序应该能读到"<tagname>"这个标签词
34
+
35
+ 例子:
36
+ | tagname |
37
+ | {名称1} |
38
+ | {名称2} |
39
+ | {00.01} |
40
+ | {00.02} |
41
+
42
+ 场景: 读取文档信息标签
43
+ 包括标题、摘要、作者、邮件等设置信息
44
+
45
+ 场景: 读取图像标签
46
+ 这是做图像替换时使用的
@@ -0,0 +1,64 @@
1
+ #language: zh-CN
2
+
3
+ 功能: 替换Docx内标签
4
+ 将docx文档中的标签替换为指定的内容。
5
+ 替换的情形有很多,大致包括:
6
+ 1、单个标签替换,如"{name}"替换为"周大福"
7
+ 2、多个标签同时替换
8
+ 3、列表标签替换,如表格中包含一行定义,每行包括"{价格}"和"{数量}",而要替换的数据是不确定的,如有5行,也可能是50行
9
+ 但所替换的数据都使用标签所在的行样式
10
+ 4、表格中可能包含一些复杂的情况,例如行样式包括按奇数行、偶数行的不同样式
11
+ 5、docx文件也可能对List列表作为整行的样式复制
12
+ 6、更复杂的情况是图表、图片等情况
13
+ 7、还有页眉、页脚中的内容替换
14
+
15
+ 背景: 被替换的源文件
16
+ 假如"docx-examples"示例文件夹中存在一个"source.docx"的文件
17
+ 而且"docx-examples/dest.docx"这个目标文件已经被清除
18
+
19
+ 场景大纲: 1、简单地读取词语替换标签
20
+ 这是最简单的情形,例如将标签{name},替换为真正的姓名。
21
+
22
+ 假如程序将目标文件中的"<tagname>"替换为"<value>"
23
+ 那么应该生成目标文件
24
+ 而且被目标文件中应该包含"<value>"这个标签词
25
+
26
+ 例子: 替换单个标签的几种情况
27
+
28
+ | tagname | value |
29
+ | {name} | 周大福 |
30
+ | {Name} | 周大福 |
31
+ | {NAME} | 周大福 |
32
+ | {{名字}} | 周大福 |
33
+ | $名字$ | 周大福 |
34
+
35
+ 场景: 2、设置多个标签的情形
36
+ 如果同时替换5个标签的,也要能正确运行
37
+
38
+ 假如有这样一组数据:
39
+ | {name} | 周大福 |
40
+ | {Name} | 周二福 |
41
+ | {NAME} | 周三福 |
42
+ | {{名字}} | 周四福 |
43
+ | $名字$ | 周五福 |
44
+
45
+ 当程序将源文件的第1列中标签替换为第2列数据
46
+ 那么应该生成目标文件
47
+ 而且被目标文件中应该包含被替换的第2列数据
48
+
49
+ @wip
50
+ 场景: 3、替换表格行数据
51
+ 按行数据替换表格内容是常见的应用
52
+
53
+ 假如有这样一组数据:
54
+ | {名称1} | {00.01} |
55
+ | 自行车 | 256.00 |
56
+ | 小汽车 | 125600 |
57
+ | 大卡车 | 256000.00 |
58
+ | 电视机 | 6999.00 |
59
+ | 洗衣机 | 3488.00 |
60
+
61
+ 当程序将表中第1行作为标签名,第2行以后作为行数据替换
62
+ 那么应该生成目标文件
63
+ 而且被目标文件中应该包含被替换的第2行以后的数据
64
+
@@ -0,0 +1,92 @@
1
+ #encoding: utf-8
2
+ lib = File.expand_path('../../../lib', __FILE__)
3
+ require "#{lib}/docx/cloner"
4
+ #require 'fileutils'
5
+
6
+ 假如(/^"(.*?)"示例文件夹中存在一个"(.*?)"的文件$/) do |folder, file|
7
+ @source_filename = File.expand_path "#{folder}/#{file}"
8
+ File.exists?(@source_filename).should be_true
9
+ end
10
+
11
+
12
+ 那么(/^程序应该能读到"(.*?)"这个标签词$/) do |tag_name|
13
+ docx = Docx::Cloner::DocxTool.new @source_filename
14
+ result = docx.include_single_tag? tag_name
15
+ docx.release
16
+ result.should be_true
17
+ end
18
+
19
+
20
+ 假如(/^"(.*?)"这个目标文件已经被清除$/) do |dest|
21
+ @dest_filename = dest
22
+ File.delete @dest_filename if File.exist?(dest)
23
+ File.exist?(dest).should be_false
24
+ end
25
+
26
+ 假如(/^程序将目标文件中的"(.*?)"替换为"(.*?)"$/) do |tag, value|
27
+ docx = Docx::Cloner::DocxTool.new @source_filename
28
+ result = docx.set_single_tag tag, value
29
+ docx.save @dest_filename
30
+ docx.release
31
+ result.should be_true
32
+ end
33
+
34
+ 那么(/^应该生成目标文件$/) do
35
+ File.exist?(@dest_filename).should be_true
36
+ end
37
+
38
+ 而且(/^被目标文件中应该包含"(.*?)"这个标签词$/) do |value|
39
+ docx = Docx::Cloner::DocxTool.new @dest_filename
40
+ result = docx.include_single_tag? value
41
+ docx.release
42
+ result.should be_true
43
+ end
44
+
45
+ 假如(/^有这样一组数据:$/) do |table|
46
+ @data = table.raw
47
+ end
48
+
49
+ 当(/^程序将源文件的第1列中标签替换为第2列数据$/) do
50
+ result = true
51
+ docx = Docx::Cloner::DocxTool.new @source_filename
52
+ @data.each do |row|
53
+ result &= docx.set_single_tag row[0], row[1]
54
+ end
55
+ docx.save @dest_filename
56
+ docx.release
57
+ result.should be_true
58
+ end
59
+
60
+ 那么(/^被目标文件中应该包含被替换的第2列数据$/) do
61
+ result = true
62
+ docx = Docx::Cloner::DocxTool.new @dest_filename
63
+ @data.each do |row|
64
+ result &= docx.include_single_tag? row[1]
65
+ end
66
+ docx.release
67
+ result.should be_true
68
+ end
69
+
70
+ 当(/^程序将表中第1行作为标签名,第2行以后作为行数据替换$/) do
71
+ docx = Docx::Cloner::DocxTool.new @source_filename
72
+
73
+ #先设置行标签的复制范围和类型
74
+ #再逐行克隆表数据
75
+ #yield块结束后清除标签
76
+ result = docx.set_row_tags @data.first, @data[1..-1], 'tr'
77
+ docx.save @dest_filename
78
+ docx.release
79
+ result.should be_true
80
+ end
81
+
82
+ 那么(/^被目标文件中应该包含被替换的第2行以后的数据$/) do
83
+ result = true
84
+ docx = Docx::Cloner::DocxTool.new @dest_filename
85
+ @data[1..-1].each do |row|
86
+ row.each do |value|
87
+ result &= docx.include_single_tag? value
88
+ end
89
+ end
90
+ docx.release
91
+ result.should be_true
92
+ end
@@ -1,7 +1,257 @@
1
+ #encoding: utf-8
1
2
  require "docx/cloner/version"
3
+ require 'zip/zip' #rubyzip gem
4
+ require 'nokogiri'
2
5
 
3
6
  module Docx
4
7
  module Cloner
5
- # Your code goes here...
8
+ class WordXmlFile
9
+ def self.open(path, &block)
10
+ self.new(path, &block)
11
+ end
12
+
13
+ def initialize(path, &block)
14
+ @replace = {}
15
+ if block_given?
16
+ @zip = Zip::ZipFile.open(path)
17
+ yield self
18
+ @zip.close
19
+ else
20
+ @zip = Zip::ZipFile.open(path)
21
+ end
22
+ end
23
+
24
+ def merge(rec)
25
+ _xml = @zip.read("word/document.xml")
26
+ doc = Nokogiri::XML(_xml)
27
+ tags = doc.root.xpath("//w:t[contains(., '_Name')]")
28
+ tags.each do |field|
29
+ new_field = field
30
+ if field.content == 'First_Name'
31
+ field.inner_html = 'Adi'
32
+ new_field.inner_html = 'My Adi'
33
+ field.add_next_sibling(new_field.to_html)
34
+ elsif field.content == 'Last_Name'
35
+ field.inner_html = 'Zhou'
36
+ end
37
+ end
38
+ @replace["word/document.xml"] = doc.serialize :save_with => 0
39
+ end
40
+
41
+ def save(path)
42
+ Zip::ZipFile.open(path, Zip::ZipFile::CREATE) do |out|
43
+ @zip.each do |entry|
44
+ out.get_output_stream(entry.name) do |o|
45
+ if @replace[entry.name]
46
+ o.write(@replace[entry.name])
47
+ else
48
+ o.write(@zip.read(entry.name))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ @zip.close
54
+ end
55
+ end
56
+
57
+ class DocxTool
58
+
59
+ '加载docx文件,将段落存储到@paragraph,用@paragraph[:text_content]检索,再从段落内检索xml标签位置'
60
+ def initialize(file)
61
+ @zip = Zip::ZipFile.open(file)
62
+ _xml = @zip.read("word/document.xml")
63
+ @doc = Nokogiri::XML(_xml)
64
+ @global_paragraph = generate_paragraph @doc
65
+
66
+ @replace = {}
67
+
68
+ #puts @paragraph
69
+ end
70
+
71
+ def release
72
+ @zip.close
73
+ end
74
+
75
+ def save(path)
76
+ @replace["word/document.xml"] = @doc.serialize :save_with => 0
77
+
78
+ Zip::ZipFile.open(path, Zip::ZipFile::CREATE) do |out|
79
+ @zip.each do |entry|
80
+ out.get_output_stream(entry.name) do |o|
81
+ if @replace[entry.name]
82
+ o.write(@replace[entry.name])
83
+ else
84
+ o.write(@zip.read(entry.name))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ def include_single_tag?(tag)
93
+ @global_paragraph.each do |p|
94
+ if p[:text_content].include? tag
95
+ return true
96
+ end
97
+ end
98
+ return false
99
+ end
100
+
101
+ def read_single_tag_xml(tag)
102
+ @global_paragraph.each do |p|
103
+ if p[:text_content].include? tag
104
+ from = p[:text_content].index tag
105
+ to = from + tag.size - 1
106
+ #puts "from:#{from}, to:#{to}"
107
+ pos = 0
108
+ dest = ""
109
+ p[:text_run].each do |wt|
110
+ #puts "pos:#{pos}"
111
+ if pos >= from && pos < to
112
+ dest << wt.parent.to_xml << "\n"
113
+ end
114
+ if pos >= to
115
+ return dest
116
+ end
117
+ pos += wt.content.size
118
+ end
119
+ return dest
120
+ end
121
+
122
+ end
123
+ return ''
124
+ end
125
+
126
+ #替换单个标签为指定值
127
+ def set_single_tag tag, value
128
+ replace_tag tag, value
129
+ end
130
+
131
+ #获取标签所在的范围,例如表格的行
132
+ #简单的考虑,则tags中第一个标签位置即可确定为scope位置
133
+ #复杂的考虑,则可根据tags中所有标签的共同根(如<w:tr>)确定scope位置,这种情况将允许标签名拥有自己的作用域
134
+ #这里仅做简单的考虑
135
+ def get_tag_scope tag, type
136
+ @global_paragraph.each do |p|
137
+ if p[:text_content].include? tag #这里是简单的考虑,即使行内标签也必须全局唯一
138
+ node = p[:text_run].first
139
+ while true
140
+ return unless node #查找父节点失败
141
+ return node if node.node_name == type #查找到匹配的父节点
142
+ node = node.parent
143
+ end
144
+ end
145
+ end
146
+ return false
147
+ end
148
+
149
+ def generate_paragraph node
150
+ paragraphs = []
151
+ puts "查找范围:#{node.path}"
152
+ wp_set = node.xpath(".//w:p")
153
+ #puts "#{wp_set.size}'s wp"
154
+ wp_set.each do |wp|
155
+ p = {text_content: '', text_run: []}
156
+ wp.xpath(".//w:t").each do |t|
157
+ p[:text_content] << t.content
158
+ p[:text_run] << t
159
+ #puts "node name: #{t.node_name}" if t.content.size > 0
160
+ #puts t.path
161
+ end
162
+ paragraphs << p
163
+ #puts p[:text_content].include? '$名字$'
164
+ end
165
+ return paragraphs
166
+ end
167
+
168
+ #在指定的范围内替换标签
169
+ def replace_tag tag, value, node=nil
170
+ paragraphs = node ? generate_paragraph(node) : @global_paragraph
171
+ #puts paragraphs
172
+ paragraphs.each do |p|
173
+ #puts p[:text_content]
174
+ if p[:text_content].include? tag
175
+ from = p[:text_content].index tag
176
+ to = from + tag.size - 1
177
+ #puts "tag:#{tag} | from:#{from}, to:#{to} >> #{p[:text_content]}"
178
+ pos = 0
179
+ dest = []
180
+ #puts p[:text_run]
181
+ p[:text_run].each do |wt|
182
+ #puts "pos:#{pos}"
183
+ #通常情况下,msword会把标签拆分成多个xml标签,如'{name}'被拆分成'<wt>{</wt>'和'<wt>name}</wt>'
184
+ #这可能跟编辑器有关,在处理中文时,这是一种常见的情形
185
+ if pos+1 >= from && pos <= to #通过pos+1修正临界点问题
186
+ dest << wt
187
+ end
188
+ if pos > to
189
+ break
190
+ end
191
+ pos += wt.content.size
192
+
193
+ #这里要处理一下标签没有被拆分的情形,而是作为纯文本被包含在某个标签中
194
+ #例如'{name}'包含在'<wt>my {name}</wt>'中
195
+ #puts "pos:#{pos}, to:#{to}, dest.size:#{dest.size}"
196
+ #puts wt
197
+ if pos >= to && dest.size == 0
198
+ #puts "simple_type | pos:#{pos}, to:#{to} >> #{wt.content}"
199
+ wt.inner_html = wt.content.sub(tag, value)
200
+ return true #如果是这种简单情形,就不再需要后续处理了
201
+ end
202
+ end
203
+
204
+ if dest.size > 0
205
+ puts "被替换节点:#{dest.first.path}"
206
+ dest.first.content = value
207
+ dest[1..-1].each do |node|
208
+ #puts node
209
+ node.remove
210
+ end
211
+ #puts "\n"
212
+ return true
213
+ else
214
+ return false
215
+ end
216
+ end
217
+
218
+ end
219
+ return false
220
+
221
+ end
222
+
223
+ #clone标签所在的范围,例如表格的行
224
+ #返回一组新的行对象集合
225
+ def clone_tag_scope node, times
226
+ #puts "clone #{node.node_name} #{times} times"
227
+ nodes = Array.new times
228
+ puts "被克隆节点:#{node.path}"
229
+ times.downto(1).each do |_i|
230
+ i = _i.to_i - 1
231
+ nodes[i] = node.dup
232
+ node.add_next_sibling nodes[i]
233
+ puts "第#{i+1}个节点克隆:#{nodes[i].path}"
234
+ end
235
+ return nodes
236
+ end
237
+
238
+ #根据行标签设置,替换成多行数据,这里考虑表格的一般情况
239
+ def set_row_tags tags, values, type
240
+ puts "tags:#{tags}, values:#{values}, type:#{type}"
241
+ #找到标签所在行的父节点
242
+ tag_scope_node = get_tag_scope tags.first, type
243
+ value_scope_nodes = clone_tag_scope tag_scope_node, values.size
244
+ value_scope_nodes.each_with_index do |node, r|
245
+ puts "查找范围:#{node.path}"
246
+ tags.each_with_index do |tag, c|
247
+ replace_tag tag, values[r][c], node
248
+ end
249
+ end
250
+ #清除标签
251
+ tag_scope_node.remove
252
+ return true
253
+ end
254
+
255
+ end
6
256
  end
7
257
  end