62b592efa3a635db2596aa1a5e9b0ca9f09a273b
[scpubgit/stemmatology.git] / base / lib / Text / Tradition / Collation / Reading.pm
1 package Text::Tradition::Collation::Reading;
2
3 use Moose;
4 use Moose::Util qw/ does_role apply_all_roles /;
5 use Moose::Util::TypeConstraints;
6 use Text::Tradition::Error;
7 use XML::Easy::Syntax qw( $xml10_name_rx $xml10_namestartchar_rx );
8 use overload '""' => \&_stringify, 'fallback' => 1;
9
10 subtype 'ReadingID',
11         as 'Str',
12         where { $_ =~ /\A$xml10_name_rx\z/ },
13         message { 'Reading ID must be a valid XML attribute string' };
14         
15 no Moose::Util::TypeConstraints;
16
17 # Enable plugin(s) if available
18 eval { with 'Text::Tradition::Morphology'; };
19 # Morphology package is not on CPAN, so don't warn of its absence
20 # if( $@ ) {
21 #       warn "Text::Tradition::Morphology not found: $@. Disabling lexeme functionality";
22 # };
23
24 =head1 NAME
25
26 Text::Tradition::Collation::Reading - represents a reading (usually a word)
27 in a collation.
28
29 =head1 DESCRIPTION
30
31 Text::Tradition is a library for representation and analysis of collated
32 texts, particularly medieval ones.  A 'reading' refers to a unit of text,
33 usually a word, that appears in one or more witnesses (manuscripts) of the
34 tradition; the text of a given witness is composed of a set of readings in
35 a particular sequence
36
37 =head1 METHODS
38
39 =head2 new
40
41 Creates a new reading in the given collation with the given attributes.
42 Options include:
43
44 =over 4
45
46 =item collation - The Text::Tradition::Collation object to which this
47 reading belongs.  Required.
48
49 =item id - A unique identifier for this reading. Required.
50
51 =item text - The word or other text of the reading.
52
53 =item is_start - The reading is the starting point for the collation.
54
55 =item is_end - The reading is the ending point for the collation.
56
57 =item is_lacuna - The 'reading' represents a known gap in the text.
58
59 =item is_ph - A temporary placeholder for apparatus parsing purposes.  Do
60 not use unless you know what you are doing.
61
62 =item rank - The sequence number of the reading. This should probably not
63 be set manually.
64
65 =back
66
67 One of 'text', 'is_start', 'is_end', or 'is_lacuna' is required.
68
69 =head2 collation
70
71 =head2 id
72
73 =head2 text
74
75 =head2 is_start
76
77 =head2 is_end
78
79 =head2 is_lacuna
80
81 =head2 rank
82
83 Accessor methods for the given attributes.
84
85 =cut
86
87 has 'collation' => (
88         is => 'ro',
89         isa => 'Text::Tradition::Collation',
90         # required => 1,
91         weak_ref => 1,
92         );
93
94 has 'id' => (
95         is => 'ro',
96         isa => 'ReadingID',
97         required => 1,
98         );
99
100 has 'text' => (
101         is => 'ro',
102         isa => 'Str',
103         required => 1,
104         writer => 'alter_text',
105         );
106         
107 has 'is_start' => (
108         is => 'ro',
109         isa => 'Bool',
110         default => undef,
111         );
112
113 has 'is_end' => (
114         is => 'ro',
115         isa => 'Bool',
116         default => undef,
117         );
118     
119 has 'is_lacuna' => (
120     is => 'ro',
121     isa => 'Bool',
122         default => undef,
123     );
124     
125 has 'is_ph' => (
126         is => 'ro',
127         isa => 'Bool',
128         default => undef,
129         );
130         
131 has 'is_common' => (
132         is => 'rw',
133         isa => 'Bool',
134         default => undef,
135         );
136
137 has 'rank' => (
138     is => 'rw',
139     isa => 'Int',
140     predicate => 'has_rank',
141     clearer => 'clear_rank',
142     );
143     
144 ## For prefix/suffix readings
145
146 has 'join_prior' => (
147         is => 'ro',
148         isa => 'Bool',
149         default => undef,
150         writer => '_set_join_prior',
151         );
152         
153 has 'join_next' => (
154         is => 'ro',
155         isa => 'Bool',
156         default => undef,
157         writer => '_set_join_next',
158         );
159
160
161 around BUILDARGS => sub {
162         my $orig = shift;
163         my $class = shift;
164         my $args;
165         if( @_ == 1 ) {
166                 $args = shift;
167         } else {
168                 $args = { @_ };
169         }
170                         
171         # If one of our special booleans is set, we change the text and the
172         # ID to match.
173         if( exists $args->{'is_lacuna'} && $args->{'is_lacuna'} && !exists $args->{'text'} ) {
174                 $args->{'text'} = '#LACUNA#';
175         } elsif( exists $args->{'is_start'} && $args->{'is_start'} ) {
176                 $args->{'id'} = '__START__';  # Change the ID to ensure we have only one
177                 $args->{'text'} = '#START#';
178                 $args->{'rank'} = 0;
179         } elsif( exists $args->{'is_end'} && $args->{'is_end'} ) {
180                 $args->{'id'} = '__END__';      # Change the ID to ensure we have only one
181                 $args->{'text'} = '#END#';
182         } elsif( exists $args->{'is_ph'} && $args->{'is_ph'} ) {
183                 $args->{'text'} = $args->{'id'};
184         }
185         
186         # Backwards compatibility for non-XMLname IDs
187         my $rid = $args->{'id'};
188         $rid =~ s/\#/__/g;
189         $rid =~ s/[\/,]/./g;
190     if( $rid !~ /^$xml10_namestartchar_rx/ ) {
191         $rid = 'r'.$rid;
192     }
193         $args->{'id'} = $rid;
194         
195         $class->$orig( $args );
196 };
197
198 # Look for a lexeme-string argument in the build args; if there, pull in the
199 # morphology role if possible.
200 sub BUILD {
201         my( $self, $args ) = @_;
202         if( exists $args->{'lexemes'} ) {
203                 unless( $self->can( '_deserialize_lexemes' ) ) {
204                         warn "No morphology package installed; DROPPING lexemes";
205                         return;
206                 }
207                 $self->_deserialize_lexemes( $args->{'lexemes'} );
208         }
209 }
210
211 =head2 is_meta
212
213 A meta attribute (ha ha), which should be true if any of our 'special'
214 booleans are true.  Implies that the reading does not represent a bit 
215 of text found in a witness.
216
217 =cut
218
219 sub is_meta {
220         my $self = shift;
221         return $self->is_start || $self->is_end || $self->is_lacuna || $self->is_ph;    
222 }
223
224 =head2 is_identical( $other_reading )
225
226 Returns true if the reading is identical to the other reading. The basic test
227 is equality of ->text attributes, but this may be wrapped or overridden by 
228 extensions.
229
230 =cut
231
232 sub is_identical {
233         my( $self, $other ) = @_;
234         return $self->text eq $other->text;
235 }
236
237 =head2 is_combinable
238
239 Returns true if the reading may in theory be combined into a multi-reading
240 segment within the collation graph. The reading must not be a meta reading,
241 and it must not have any relationships in its own right with any others.
242 This test may be wrapped or overridden by extensions.
243
244 =cut
245
246 sub is_combinable {
247         my $self = shift;
248         return undef if $self->is_meta;
249         return !$self->related_readings();
250 }
251
252 # Not really meant for public consumption. Adopt the text of the other reading
253 # into this reading.
254 sub _combine {
255         my( $self, $other, $joinstr ) = @_;
256         $self->alter_text( join( $joinstr, $self->text, $other->text ) );
257         # Change this reading to a joining one if necessary
258         $self->_set_join_next( $other->join_next );
259 }
260
261 =head1 Convenience methods
262
263 =head2 related_readings
264
265 Calls Collation's related_readings with $self as the first argument.
266
267 =cut
268
269 sub related_readings {
270         my $self = shift;
271         return $self->collation->related_readings( $self, @_ );
272 }
273
274 =head2 witnesses 
275
276 Calls Collation's reading_witnesses with $self as the first argument.
277
278 =cut
279
280 sub witnesses {
281         my $self = shift;
282         return $self->collation->reading_witnesses( $self, @_ );
283 }
284
285 =head2 predecessors
286
287 Returns a list of Reading objects that immediately precede $self in the collation.
288
289 =cut
290
291 sub predecessors {
292         my $self = shift;
293         my @pred = $self->collation->sequence->predecessors( $self->id );
294         return map { $self->collation->reading( $_ ) } @pred;
295 }
296
297 =head2 successors
298
299 Returns a list of Reading objects that immediately follow $self in the collation.
300
301 =cut
302
303 sub successors {
304         my $self = shift;
305         my @succ = $self->collation->sequence->successors( $self->id );
306         return map { $self->collation->reading( $_ ) } @succ;
307 }
308
309 ## Utility methods
310
311 sub _stringify {
312         my $self = shift;
313         return $self->id;
314 }
315
316 sub TO_JSON {
317         my $self = shift;
318         return $self->text;
319 }
320
321 sub throw {
322         Text::Tradition::Error->throw( 
323                 'ident' => 'Reading error',
324                 'message' => $_[0],
325                 );
326 }
327
328 no Moose;
329 __PACKAGE__->meta->make_immutable;
330
331 1;