Add logic for normalized form of lemma to propagate on orth/spelling links
[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 Text::Tradition::Datatypes;
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 # Enable plugin(s) if available
11 eval { with 'Text::Tradition::Morphology'; };
12 # Morphology package is not on CPAN, so don't warn of its absence
13 # if( $@ ) {
14 #       warn "Text::Tradition::Morphology not found: $@. Disabling lexeme functionality";
15 # };
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 'is_lemma' => (
101         is => 'ro',
102         isa => 'Bool',
103         default => undef,
104         writer => 'make_lemma',
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 around make_lemma => sub {
212         my $orig = shift;
213         my $self = shift;
214         my $val = shift;
215
216         # TODO unset the lemma from any other reading at the same rank.
217         if( $val && $self->does( 'Text::Tradition::Morphology' )) {
218                 # Set the normal form on all orthographically related readings to match
219                 # the normal form on this one.
220                 my $filter = sub { 
221                         my $rl = shift; 
222                         my $rltype = $self->collation->relations->type( $rl->type );
223                         return $rltype->bindlevel < 2 
224                 };
225                 foreach my $r ( $self->related_readings( $filter ) ) {
226                         $r->normal_form( $self->normal_form );
227                 }
228         }
229         $self->$orig( $val );
230 };
231
232 =head2 is_meta
233
234 A meta attribute (ha ha), which should be true if any of our 'special'
235 booleans are true.  Implies that the reading does not represent a bit 
236 of text found in a witness.
237
238 =cut
239
240 sub is_meta {
241         my $self = shift;
242         return $self->is_start || $self->is_end || $self->is_lacuna || $self->is_ph;    
243 }
244
245 =head2 is_identical( $other_reading )
246
247 Returns true if the reading is identical to the other reading. The basic test
248 is equality of ->text attributes, but this may be wrapped or overridden by 
249 extensions.
250
251 =cut
252
253 sub is_identical {
254         my( $self, $other ) = @_;
255         return $self->text eq $other->text;
256 }
257
258 =head2 is_combinable
259
260 Returns true if the reading may in theory be combined into a multi-reading
261 segment within the collation graph. The reading must not be a meta reading,
262 and it must not have any relationships in its own right with any others.
263 This test may be wrapped or overridden by extensions.
264
265 =cut
266
267 sub is_combinable {
268         my $self = shift;
269         return undef if $self->is_meta;
270         return !$self->related_readings();
271 }
272
273 # Not really meant for public consumption. Adopt the text of the other reading
274 # into this reading.
275 sub _combine {
276         my( $self, $other, $joinstr ) = @_;
277         $self->alter_text( join( $joinstr, $self->text, $other->text ) );
278         # Change this reading to a joining one if necessary
279         $self->_set_join_next( $other->join_next );
280 }
281
282 =head1 Convenience methods
283
284 =head2 related_readings
285
286 Calls Collation's related_readings with $self as the first argument.
287
288 =cut
289
290 sub related_readings {
291         my $self = shift;
292         return $self->collation->related_readings( $self, @_ );
293 }
294
295 =head2 witnesses 
296
297 Calls Collation's reading_witnesses with $self as the first argument.
298
299 =cut
300
301 sub witnesses {
302         my $self = shift;
303         return $self->collation->reading_witnesses( $self, @_ );
304 }
305
306 =head2 predecessors
307
308 Returns a list of Reading objects that immediately precede $self in the collation.
309
310 =cut
311
312 sub predecessors {
313         my $self = shift;
314         my @pred = $self->collation->sequence->predecessors( $self->id );
315         return map { $self->collation->reading( $_ ) } @pred;
316 }
317
318 =head2 successors
319
320 Returns a list of Reading objects that immediately follow $self in the collation.
321
322 =cut
323
324 sub successors {
325         my $self = shift;
326         my @succ = $self->collation->sequence->successors( $self->id );
327         return map { $self->collation->reading( $_ ) } @succ;
328 }
329
330 ## Utility methods
331
332 sub _stringify {
333         my $self = shift;
334         return $self->id;
335 }
336
337 sub TO_JSON {
338         my $self = shift;
339         return $self->text;
340 }
341
342 sub throw {
343         Text::Tradition::Error->throw( 
344                 'ident' => 'Reading error',
345                 'message' => $_[0],
346                 );
347 }
348
349 no Moose;
350 __PACKAGE__->meta->make_immutable;
351
352 1;