6852a341c345c8a313266a817681afcaed827257
[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 =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;