a482b604b4da70ac9aae4465ac2692e8f5cd4987
[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_lemma' => (
108         is => 'ro',
109         isa => 'Bool',
110         default => undef,
111         writer => 'make_lemma',
112         );
113         
114 has 'is_start' => (
115         is => 'ro',
116         isa => 'Bool',
117         default => undef,
118         );
119
120 has 'is_end' => (
121         is => 'ro',
122         isa => 'Bool',
123         default => undef,
124         );
125     
126 has 'is_lacuna' => (
127     is => 'ro',
128     isa => 'Bool',
129         default => undef,
130     );
131     
132 has 'is_ph' => (
133         is => 'ro',
134         isa => 'Bool',
135         default => undef,
136         );
137         
138 has 'is_common' => (
139         is => 'rw',
140         isa => 'Bool',
141         default => undef,
142         );
143
144 has 'rank' => (
145     is => 'rw',
146     isa => 'Int',
147     predicate => 'has_rank',
148     clearer => 'clear_rank',
149     );
150     
151 ## For prefix/suffix readings
152
153 has 'join_prior' => (
154         is => 'ro',
155         isa => 'Bool',
156         default => undef,
157         writer => '_set_join_prior',
158         );
159         
160 has 'join_next' => (
161         is => 'ro',
162         isa => 'Bool',
163         default => undef,
164         writer => '_set_join_next',
165         );
166
167
168 around BUILDARGS => sub {
169         my $orig = shift;
170         my $class = shift;
171         my $args;
172         if( @_ == 1 ) {
173                 $args = shift;
174         } else {
175                 $args = { @_ };
176         }
177                         
178         # If one of our special booleans is set, we change the text and the
179         # ID to match.
180         if( exists $args->{'is_lacuna'} && $args->{'is_lacuna'} && !exists $args->{'text'} ) {
181                 $args->{'text'} = '#LACUNA#';
182         } elsif( exists $args->{'is_start'} && $args->{'is_start'} ) {
183                 $args->{'id'} = '__START__';  # Change the ID to ensure we have only one
184                 $args->{'text'} = '#START#';
185                 $args->{'rank'} = 0;
186         } elsif( exists $args->{'is_end'} && $args->{'is_end'} ) {
187                 $args->{'id'} = '__END__';      # Change the ID to ensure we have only one
188                 $args->{'text'} = '#END#';
189         } elsif( exists $args->{'is_ph'} && $args->{'is_ph'} ) {
190                 $args->{'text'} = $args->{'id'};
191         }
192         
193         # Backwards compatibility for non-XMLname IDs
194         my $rid = $args->{'id'};
195         $rid =~ s/\#/__/g;
196         $rid =~ s/[\/,]/./g;
197     if( $rid !~ /^$xml10_namestartchar_rx/ ) {
198         $rid = 'r'.$rid;
199     }
200         $args->{'id'} = $rid;
201         
202         $class->$orig( $args );
203 };
204
205 # Look for a lexeme-string argument in the build args; if there, pull in the
206 # morphology role if possible.
207 sub BUILD {
208         my( $self, $args ) = @_;
209         if( exists $args->{'lexemes'} ) {
210                 unless( $self->can( '_deserialize_lexemes' ) ) {
211                         warn "No morphology package installed; DROPPING lexemes";
212                         return;
213                 }
214                 $self->_deserialize_lexemes( $args->{'lexemes'} );
215         }
216 }
217
218 =head2 is_meta
219
220 A meta attribute (ha ha), which should be true if any of our 'special'
221 booleans are true.  Implies that the reading does not represent a bit 
222 of text found in a witness.
223
224 =cut
225
226 sub is_meta {
227         my $self = shift;
228         return $self->is_start || $self->is_end || $self->is_lacuna || $self->is_ph;    
229 }
230
231 =head2 is_identical( $other_reading )
232
233 Returns true if the reading is identical to the other reading. The basic test
234 is equality of ->text attributes, but this may be wrapped or overridden by 
235 extensions.
236
237 =cut
238
239 sub is_identical {
240         my( $self, $other ) = @_;
241         return $self->text eq $other->text;
242 }
243
244 =head2 is_combinable
245
246 Returns true if the reading may in theory be combined into a multi-reading
247 segment within the collation graph. The reading must not be a meta reading,
248 and it must not have any relationships in its own right with any others.
249 This test may be wrapped or overridden by extensions.
250
251 =cut
252
253 sub is_combinable {
254         my $self = shift;
255         return undef if $self->is_meta;
256         return !$self->related_readings();
257 }
258
259 # Not really meant for public consumption. Adopt the text of the other reading
260 # into this reading.
261 sub _combine {
262         my( $self, $other, $joinstr ) = @_;
263         $self->alter_text( join( $joinstr, $self->text, $other->text ) );
264         # Change this reading to a joining one if necessary
265         $self->_set_join_next( $other->join_next );
266 }
267
268 =head1 Convenience methods
269
270 =head2 related_readings
271
272 Calls Collation's related_readings with $self as the first argument.
273
274 =cut
275
276 sub related_readings {
277         my $self = shift;
278         return $self->collation->related_readings( $self, @_ );
279 }
280
281 =head2 witnesses 
282
283 Calls Collation's reading_witnesses with $self as the first argument.
284
285 =cut
286
287 sub witnesses {
288         my $self = shift;
289         return $self->collation->reading_witnesses( $self, @_ );
290 }
291
292 =head2 predecessors
293
294 Returns a list of Reading objects that immediately precede $self in the collation.
295
296 =cut
297
298 sub predecessors {
299         my $self = shift;
300         my @pred = $self->collation->sequence->predecessors( $self->id );
301         return map { $self->collation->reading( $_ ) } @pred;
302 }
303
304 =head2 successors
305
306 Returns a list of Reading objects that immediately follow $self in the collation.
307
308 =cut
309
310 sub successors {
311         my $self = shift;
312         my @succ = $self->collation->sequence->successors( $self->id );
313         return map { $self->collation->reading( $_ ) } @succ;
314 }
315
316 ## Utility methods
317
318 sub _stringify {
319         my $self = shift;
320         return $self->id;
321 }
322
323 sub TO_JSON {
324         my $self = shift;
325         return $self->text;
326 }
327
328 sub throw {
329         Text::Tradition::Error->throw( 
330                 'ident' => 'Reading error',
331                 'message' => $_[0],
332                 );
333 }
334
335 no Moose;
336 __PACKAGE__->meta->make_immutable;
337
338 1;