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