docx-cloner 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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