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