1Text::WordDiff(3pm)   User Contributed Perl Documentation  Text::WordDiff(3pm)
2
3
4

Name

6       Text::WordDiff - Track changes between documents
7

Synopsis

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

Description

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 = '뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐';
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                   '뼈',
107                   '뼉',
108                   '뼘',
109                   '뼙',
110                   '뼛',
111                   '뼜',
112                   '뼝',
113                   '뽀',
114                   '뽁',
115                   '뽄',
116                   '뽈',
117                   '뽐',
118                   '뽑',
119                   '뽕',
120                   '뾔',
121                   '뾰',
122                   '뿅',
123                   '뿌',
124                   '뿍',
125                   '뿐',
126                   '뿔',
127                   '뿜',
128                   '뿟',
129                   '뿡',
130                   '?',
131                   '?쁑',
132                   '쁘',
133                   '쁜',
134                   '쁠',
135                   '쁨',
136                   '쁩',
137                   '삐'
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

Options

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 file_footer()
190           methods. Defaults to "ANSIColor" for nice display of diffs in an
191           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 then
195           instantiate an object of that class, passing in the options hash
196           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() returns
207           a Unified diff-style header. All of the other formatting methods
208           simply return empty strings, and are therefore ripe for overriding.
209
210       •   FILENAME_A, MTIME_A, FILENAME_B, MTIME_B
211
212           The name of the file and the modification time "files" in epoch
213           seconds.  Unless a defined value is specified for these options,
214           they will be filled in for each file when word_diff() is passed a
215           filename. If a filename is not passed in and "FILENAME_A" and
216           "FILENAME_B" are not defined, the header will not be printed by the
217           base formatting base class.
218
219       •   OUTPUT
220
221           The method by which diff output should be, well, output. Examples
222           and their equivalent subroutines:
223
224               OUTPUT => \*FOOHANDLE,   # like: sub { print FOOHANDLE shift() }
225               OUTPUT => \$output,      # like: sub { $output .= shift }
226               OUTPUT => \@output,      # like: sub { push @output, shift }
227               OUTPUT => sub { $output .= shift },
228
229           If "OUTPUT" is not defined, word_diff() will simply return the diff
230           as a string. If "OUTPUT" is a code reference, it will be called
231           once with the file header, once for each hunk body, and once for
232           each piece of content. If "OUTPUT" is an IO::Handle-derived object,
233           output will be sent to that handle.
234
235       •   FILENAME_PREFIX_A, FILENAME_PREFIX_B
236
237           The string to print before the filename in the header. Defaults are
238           "---", "+++".
239
240       •   DIFF_OPTS
241
242           A hash reference to be passed as the options to
243           "Algorithm::Diff->new".  See Algorithm::Diff for details on
244           available options.
245

Formatting Classes

247       Text::WordDiff comes with two formatting classes:
248
249       Text::WordDiff::ANSIColor
250           This is the default formatting class. It emits a header and then
251           the diff content, with deleted text in bodfaced red and inserted
252           text in boldfaced green.
253
254       Text::WordDiff::HTML
255           Specify "STYLE => 'HTML'" to take advantage of this formatting
256           class. It outputs the diff content as XHTML, with deleted text in
257           "<del>" elements and inserted text in "<ins>" elements.
258
259       To implement your own formatting class, simply inherit from
260       Text::WordDiff::Base and override its methods as necssary. By default,
261       only the file_header() formatting method returns a value. All others
262       simply return empty strings, and are therefore ripe for overriding:
263
264         package My::WordDiff::Format;
265         use base 'Text::WordDiff::Base';
266
267         sub file_footer { return "End of diff\n"; }
268
269       The methods supplied by the base class are:
270
271       new()
272           Constructs and returns a new formatting object. It takes a single
273           hash reference as its argument, and uses it to construct the
274           object. The nice thing about this is that if you want to support
275           other options in your formatting class, you can just use them in
276           the formatting object constructed by the Text::WordDiff::Base class
277           and document that they can be passed as part of the options hash
278           refernce to word_diff().
279
280       file_header()
281           Called once for a single call to word_diff(), this method outputs
282           the header for the whole diff. This is the only formatting method
283           in the base class that returns anything other than an empty string.
284           It collects the filenames from filname_a() and filename_b() and, if
285           they're defined, uses the relevant prefixes and modification times
286           to return a unified diff-style header.
287
288       hunk_header()
289           This method is called for each diff hunk. It should output any
290           necessary header for the hunk.
291
292       same_items()
293           This method is called for items that have not changed between the
294           two sequnces being compared. The unchanged items will be passed as
295           a list to the method.
296
297       "delete_items"
298           This method is called for items in the first sequence that are not
299           present in the second sequcne. The deleted items will be passed as
300           a list to the method.
301
302       "insert_items"
303           This method is called for items in the second sequence that are not
304           present in the first sequcne. The inserted items will be passed as
305           a list to the method.
306
307       "hunk_footer"
308           This method is called at the end of a hunk. It should output any
309           necessary content to close out the hunk.
310
311       file_footer()
312           This method is called once when the whole diff has been procssed.
313           It should output any necessary content to close out the diff file.
314
315       "filename_a"
316           This accessor returns the value specified for the "FILENAME_A"
317           option to word_diff().
318
319       "filename_b"
320           This accessor returns the value specified for the "FILENAME_B"
321           option to word_diff().
322
323       "mtime_a"
324           This accessor returns the value specified for the "MTIME_A" option
325           to word_diff().
326
327       "mtime_b"
328           This accessor returns the value specified for the "MTIME_B" option
329           to word_diff().
330
331       "filename_prefix_a"
332           This accessor returns the value specified for the
333           "FILENAME_PREFIX_A" option to word_diff().
334
335       "filename_prefix_b"
336           This accessor returns the value specified for the
337           "FILENAME_PREFIX_B" option to word_diff().
338

See Also

340       Text::Diff
341           Inspired the interface and implementation of this module. Thanks
342           Barry!
343
344       Text::ParagraphDiff
345           A module that attempts to diff paragraphs and the words in them.
346
347       Algorithm::Diff
348           The module that makes this all possible.
349

Support

351       This module is stored in an open GitHub repository
352       <http://github.com/theory/text-worddiff/>. Feel free to fork and
353       contribute!
354
355       Please file bug reports via GitHub Issues
356       <http://github.com/theory/text-worddiff/issues/> or by sending mail to
357       bug-Text-WordDiff@rt.cpan.org <mailto:bug-Text-WordDiff@rt.cpan.org>.
358

Author

360       David E. Wheeler <david@justatheory.com>
361
362       Currently maintained by the developers of The Perl Shop <tps@cpan.org>.
363
365       Copyright (c) 2005-2011 David E. Wheeler. Some Rights Reserved.
366
367       This module is free software; you can redistribute it and/or modify it
368       under the same terms as Perl itself.
369
370
371
372perl v5.38.0                      2023-07-21               Text::WordDiff(3pm)
Impressum