1Text::WordDiff(3) User Contributed Perl Documentation Text::WordDiff(3)
2
3
4
6 Text::WordDiff - Track changes between documents
7
9 use Text::WordDiff;
10
11 my $diff = word_diff 'file1.txt', 'file2.txt', { STYLE => 'HTML' };
12 my $diff = word_diff \$string1, \$string2, { STYLE => 'ANSIColor' };
13 my $diff = word_diff \*FH1, \*FH2; \%options;
14 my $diff = word_diff \&reader1, \&reader2;
15 my $diff = word_diff \@records1, \@records2;
16
17 # May also mix input types:
18 my $diff = word_diff \@records1, 'file_B.txt';
19
21 This module is a variation on the lovely Text::Diff module. Rather
22 than generating traditional line-oriented diffs, however, it generates
23 word-oriented diffs. This can be useful for tracking changes in
24 narrative documents or documents with very long lines. To diff source
25 code, one is still best off using Text::Diff. But if you want to see
26 how a short story changed from one version to the next, this module
27 will do the job very nicely.
28
29 What is a Word?
30 I'm glad you asked! Well, sort of. It's a really hard question to
31 answer. I consulted a number of sources, but really just did my best to
32 punt on the question by reformulating it as, "How do I split text up
33 into individual words?" The short answer is to split on word
34 boundaries. However, every word has two boundaries, one at the
35 beginning and one at the end. So splitting on "/\b/" didn't work so
36 well. What I really wanted to do was to split on the beginning of every
37 word. Fortunately, _Mastering Regular Expressions_ has a recipe for
38 that: "/(?<!\w)(?=\w)/". I've borrowed this regular expression for use
39 in Perls before 5.6.x, but go for the Unicode variant in 5.6.0 and
40 newer: "/(?<!\p{IsWord})(?=\p{IsWord})/". Adding some additional
41 controls for punctuation and control characters, this sentence, for
42 example, would be split up into the following tokens:
43
44 my @words = (
45 "Adding ",
46 "some ",
47 "additional ",
48 "controls",
49 "\n",
50 "for ",
51 "punctuation ",
52 "and ",
53 "control ",
54 "characters",
55 ", ",
56 "this ",
57 "sentence",
58 ", ",
59 "for ",
60 "example",
61 ", ",
62 "would ",
63 "be",
64 "\n",
65 "split ",
66 "up ",
67 "into ",
68 "the ",
69 "following ",
70 "tokens",
71 ":",
72 );
73
74 So it's not just comparing words, but word-like tokens and
75 control/punctuation tokens. This makes sense to me, at least, as the
76 diff is between these tokens, and thus leads to a nice word-and-space-
77 and-punctuation type diff. It's not unlike what a word processor might
78 do (although a lot of them are character-based, but that seemed a bit
79 extreme--feel free to dupe this module into Text::CharDiff!).
80
81 Now, I acknowledge that there are localization issues with this
82 approach. In particular, it will fail with Chinese, Japanese, and
83 Korean text, as these languages don't put non-word characters between
84 words. Ideally, Test::WordDiff would then split on every character
85 (since a single character often equals a word), but such is not the
86 case when the "utf8" flag is set on a string. For example, This simple
87 script:
88
89 use strict;
90 use utf8;
91 use Data::Dumper;
92 my $string = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
93 my @tokens = split /(?<!\p{IsWord})(?=\p{IsWord})/msx, $string;
94 print Dumper \@tokens;
95
96 Outputs:
97
98 $VAR1 = [
99 "\x{bf08}\x{bf09}\x{bf18}\x{bf19}\x{bf1b}\x{bf1c}\x{bf1d}\x{bf40}\x{bf41}\x{bf44}\x{bf48}\x{bf50}\x{bf51}\x{bf55}\x{bf94}\x{bfb0}\x{bfc5}\x{bfcc}\x{bfcd}\x{bfd0}\x{bfd4}\x{bfdc}\x{bfdf}\x{bfe1}\x{c03c}\x{c051}\x{c058}\x{c05c}\x{c060}\x{c068}\x{c069}\x{c090}"
100 ];
101
102 Not so useful. It seems to be less of a problem if the "use utf8;" line
103 is commented out, in which case we get:
104
105 $VAR1 = [
106 'X',
107 'X',
108 'X',
109 'X',
110 'X',
111 'X',
112 'X',
113 'X',
114 'X',
115 'X',
116 'X',
117 'X',
118 'X',
119 'X',
120 'X',
121 'X',
122 'X',
123 'X',
124 'X',
125 'X',
126 'X',
127 'X',
128 'X',
129 'X',
130 '?',
131 '?X',
132 'X',
133 'X',
134 'X',
135 'X',
136 'X',
137 'X'
138 ];
139
140 Someone whose more familiar with non-space-using languages will have to
141 explain to me how I might be able to duplicate this pattern within the
142 scope of "use utf8;", seing as it may very well be important to have it
143 on in order to ensure proper character semantics.
144
145 However, if my word tokenization approach is just too naive, and you
146 decide that you need to take a different approach (maybe use
147 Lingua::ZH::Toke or similar module), you can still use this module;
148 you'll just have to tokenize your strings into words yourself, and pass
149 them to word_diff() as array references:
150
151 word_diff \@my_words1, \@my_words2;
152
154 word_diff() takes two arguments from which to draw input and an
155 optional hash reference of options to control its output. The first two
156 arguments contain the data to be diffed, and each may be in the form of
157 any of the following (that is, they can be in two different formats):
158
159 • String
160
161 A bare scalar will be assumed to be a file name. The file will be
162 opened and split up into words. word_diff() will also "stat" the
163 file to get the last modified time for use in the header, unless
164 the relevant option ("MTIME_A" or "MTIME_B") has been specified
165 explicitly.
166
167 • Scalar Reference
168
169 A scalar reference will be assumed to refer to a string. That
170 string will be split up into words.
171
172 • Array Reference
173
174 An array reference will be assumed to be a list of words.
175
176 • File Handle
177
178 A glob or IO::Handle-derived object will be read from and split up
179 into its constituent words.
180
181 The optional hash reference may contain the following options.
182 Additional options may be specified by the formattting class; see the
183 specific class for details.
184
185 • STYLE
186
187 "ANSIColor", "HTML" or an object or class name for a class
188 providing "file_header()", "hunk_header()", "same_items()",
189 "delete_items()", "insert_items()", "hunk_footer()" and
190 "file_footer()" methods. Defaults to "ANSIColor" for nice display
191 of diffs in an ANSI Color-supporting terminal.
192
193 If the package indicated by the "STYLE" has no "new()" method,
194 "word_diff()" will load it automatically (lazy loading). It will
195 then instantiate an object of that class, passing in the options
196 hash reference with which the formatting class can initialize the
197 object.
198
199 Styles may be specified as class names ("STYLE => "My::Foo""), in
200 which case they will be instantiated by calling the "new()"
201 construcctor and passing in the options hash reference, or as
202 objects ("STYLE => My::Foo->new").
203
204 The simplest way to implement your own formatting style is to
205 create a new class that inherits from Text::WordDiff::Base, wherein
206 the "new()" method is already provided, and the "file_header()"
207 returns a Unified diff-style header. All of the other formatting
208 methods simply return empty strings, and are therefore ripe for
209 overriding.
210
211 • FILENAME_A, MTIME_A, FILENAME_B, MTIME_B
212
213 The name of the file and the modification time "files" in epoch
214 seconds. Unless a defined value is specified for these options,
215 they will be filled in for each file when word_diff() is passed a
216 filename. If a filename is not passed in and "FILENAME_A" and
217 "FILENAME_B" are not defined, the header will not be printed by the
218 base formatting base class.
219
220 • OUTPUT
221
222 The method by which diff output should be, well, output. Examples
223 and their equivalent subroutines:
224
225 OUTPUT => \*FOOHANDLE, # like: sub { print FOOHANDLE shift() }
226 OUTPUT => \$output, # like: sub { $output .= shift }
227 OUTPUT => \@output, # like: sub { push @output, shift }
228 OUTPUT => sub { $output .= shift },
229
230 If "OUTPUT" is not defined, word_diff() will simply return the diff
231 as a string. If "OUTPUT" is a code reference, it will be called
232 once with the file header, once for each hunk body, and once for
233 each piece of content. If "OUTPUT" is an IO::Handle-derived object,
234 output will be sent to that handle.
235
236 • FILENAME_PREFIX_A, FILENAME_PREFIX_B
237
238 The string to print before the filename in the header. Defaults are
239 "---", "+++".
240
241 • DIFF_OPTS
242
243 A hash reference to be passed as the options to
244 "Algorithm::Diff->new". See Algorithm::Diff for details on
245 available options.
246
248 Text::WordDiff comes with two formatting classes:
249
250 Text::WordDiff::ANSIColor
251 This is the default formatting class. It emits a header and then
252 the diff content, with deleted text in bodfaced red and inserted
253 text in boldfaced green.
254
255 Text::WordDiff::HTML
256 Specify "STYLE => 'HTML'" to take advantage of this formatting
257 class. It outputs the diff content as XHTML, with deleted text in
258 "<del>" elements and inserted text in "<ins>" elements.
259
260 To implement your own formatting class, simply inherit from
261 Text::WordDiff::Base and override its methods as necssary. By default,
262 only the "file_header()" formatting method returns a value. All others
263 simply return empty strings, and are therefore ripe for overriding:
264
265 package My::WordDiff::Format;
266 use base 'Text::WordDiff::Base';
267
268 sub file_footer { return "End of diff\n"; }
269
270 The methods supplied by the base class are:
271
272 "new()"
273 Constructs and returns a new formatting object. It takes a single
274 hash reference as its argument, and uses it to construct the
275 object. The nice thing about this is that if you want to support
276 other options in your formatting class, you can just use them in
277 the formatting object constructed by the Text::WordDiff::Base class
278 and document that they can be passed as part of the options hash
279 refernce to word_diff().
280
281 "file_header()"
282 Called once for a single call to "word_diff()", this method outputs
283 the header for the whole diff. This is the only formatting method
284 in the base class that returns anything other than an empty string.
285 It collects the filenames from "filname_a()" and "filename_b()"
286 and, if they're defined, uses the relevant prefixes and
287 modification times to return a unified diff-style header.
288
289 "hunk_header()"
290 This method is called for each diff hunk. It should output any
291 necessary header for the hunk.
292
293 "same_items()"
294 This method is called for items that have not changed between the
295 two sequnces being compared. The unchanged items will be passed as
296 a list to the method.
297
298 "delete_items"
299 This method is called for items in the first sequence that are not
300 present in the second sequcne. The deleted items will be passed as
301 a list to the method.
302
303 "insert_items"
304 This method is called for items in the second sequence that are not
305 present in the first sequcne. The inserted items will be passed as
306 a list to the method.
307
308 "hunk_footer"
309 This method is called at the end of a hunk. It should output any
310 necessary content to close out the hunk.
311
312 "file_footer()"
313 This method is called once when the whole diff has been procssed.
314 It should output any necessary content to close out the diff file.
315
316 "filename_a"
317 This accessor returns the value specified for the "FILENAME_A"
318 option to word_diff().
319
320 "filename_b"
321 This accessor returns the value specified for the "FILENAME_B"
322 option to word_diff().
323
324 "mtime_a"
325 This accessor returns the value specified for the "MTIME_A" option
326 to word_diff().
327
328 "mtime_b"
329 This accessor returns the value specified for the "MTIME_B" option
330 to word_diff().
331
332 "filename_prefix_a"
333 This accessor returns the value specified for the
334 "FILENAME_PREFIX_A" option to word_diff().
335
336 "filename_prefix_b"
337 This accessor returns the value specified for the
338 "FILENAME_PREFIX_B" option to word_diff().
339
341 Text::Diff
342 Inspired the interface and implementation of this module. Thanks
343 Barry!
344
345 Text::ParagraphDiff
346 A module that attempts to diff paragraphs and the words in them.
347
348 Algorithm::Diff
349 The module that makes this all possible.
350
352 This module is stored in an open GitHub repository
353 <http://github.com/theory/text-worddiff/>. Feel free to fork and
354 contribute!
355
356 Please file bug reports via GitHub Issues
357 <http://github.com/theory/text-worddiff/issues/> or by sending mail to
358 bug-Text-WordDiff@rt.cpan.org <mailto:bug-Text-WordDiff@rt.cpan.org>.
359
361 David E. Wheeler <david@justatheory.com>
362
363 Currently maintained by the developers of The Perl Shop <tps@cpan.org>.
364
366 Copyright (c) 2005-2011 David E. Wheeler. Some Rights Reserved.
367
368 This module is free software; you can redistribute it and/or modify it
369 under the same terms as Perl itself.
370
371
372
373perl v5.36.0 2022-07-22 Text::WordDiff(3)