c82d7de40143f40ba712a02826f102770eddf80b
[Evergreen.git] / build / i18n / scripts / marc_tooltip_maker.py
1 #!/usr/bin/env python
2 # vim: set fileencoding=utf-8 :
3 # vim:et:ts=4:sw=4:
4
5 # Copyright (C) 2008 Laurentian University
6 # Dan Scott <dscott@laurentian.ca>
7
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # as published by the Free Software Foundation; either version 2
11 # of the License, or (at your option) any later version.
12
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
21
22 """
23 The MARC editor offers tooltips generated from the Library of Congress Concise
24 MARC Record documentation available online.
25
26 This script generates a French version of those tooltips based on the Library
27 and Archives Canada translation of the LoC documentation.
28 """
29
30 from BeautifulSoup import BeautifulSoup
31
32 # Simple case:
33 # Get <a id="#mrcb(###)">: map $1 to tag attribute
34 #   From within that A event, retrieve the SMALL event
35 #     If SMALL.cdata == '\s*(R)\s*' then repeatable = yes  
36 #     If SMALL.cdata == '\s*(NR)\s*' then repeatable = no
37 #   Get the next P event: map to <description> element
38 #
39 # Target:
40 #  <field repeatable="true" tag="006">
41 #    <description>This field contains 18 character positions (00-17)
42 #    that provide for coding information about special aspects of
43 #    the item being cataloged that cannot be coded in field 008
44 #    (Fixed-Length Data Elements). It is used in cases when an item
45 #    has multiple characteristics. It is also used to record the coded
46 #    serial aspects of nontextual continuing resources.</description>
47 #  </field>
48
49 # Complex case:
50
51 # field and tag and repeatable description as above
52 # check for <h3>Indicateurs</h3> before next <h2>
53 #   check for <li>Premier indicateur or <li>Second indicateur to set indicator.position
54 #   check for <li class="sqf">(\d)\s*-\s*([^<]*)< for indicator.position.value = def__init__ion
55 #   ignore if "Non d&#233;fini"
56 # check for <h3>Codes do sous-zones
57 #   for each <li>:
58 #     CDATA (stripped of tags, with (NR) or (R) stripped out) = field.subfield.def__init__ion
59 #     (NR) or (R) means field.subfield.repeatable = false or true
60
61 #  <field repeatable="true" tag="800">
62 #    <description>An author/title series added entry in which the
63 #      author portion is a personal name.</description>
64 #    <indicator position="1" value="0">
65 #      <description>Forename</description>
66 #    </indicator>
67 #    <indicator position="1" value="1">
68 #      <description>Surname</description>
69 #    </indicator>
70 #    <indicator position="1" value="3">
71 #      <description>Family name</description>
72 #    </indicator>
73 #    <subfield code="a" repeatable="false">
74 #      <description>Personal name </description>
75 #    </subfield>
76 #    <subfield code="b" repeatable="false">
77 #      <description>Numeration </description>
78 #    </subfield>
79
80 class MarcCollection(object):
81     """
82     Contains a set of descriptions of MARC fields organized by tag
83     """
84     
85     def __init__(self):
86         self.fields = {}
87
88     def add_field(self, field):
89         """
90         Add a MARC field to our collection
91         """
92         self.fields[field.tag] = field
93
94     def to_xml(self):
95         """
96         Convert the MARC field collection to XML representation
97         """
98         xml = "<?xml version='1.0' encoding='utf-8'?>\n"
99         xml += "<fields>\n"
100         keys = self.fields.keys()
101         keys.sort()
102         for key in keys:
103             xml += self.fields[key].to_xml()
104         xml += "\n</fields>\n"
105         return xml
106
107 class MarcField(object):
108     """
109     Describes the properties of a MARC field
110
111     You can directly access and manipulate the indicators and subfields lists
112     """
113     def __init__(self, tag, repeatable, description):
114         self.tag = tag
115         self.repeatable = repeatable
116         self.description = description
117         self.indicators = []
118         self.subfields = []
119
120     def to_xml(self):
121         """
122         Convert the MARC field to XML representation
123         """
124         xml = u"  <field repeatable='%s' tag='%s'>\n" % (self.repeatable, self.tag)
125         xml += u"    <description>%s</description>\n" % (self.description)
126         for ind in self.indicators:
127             xml += ind.to_xml()
128             xml += '\n'
129         for subfield in self.subfields:
130             xml += subfield.to_xml()
131             xml += '\n'
132         xml += u"  </field>\n"
133
134         return xml
135
136 class Subfield(object):
137     """
138     Describes the properties of a MARC subfield
139     """
140     def __init__(self, code, repeatable, description):
141         self.code = code
142         self.repeatable = repeatable
143         self.description = description
144
145     def to_xml(self):
146         """
147         Convert the subfield to XML representation
148         """
149         xml = u"    <subfield code='%s' repeatable='%s'>\n" % (self.code, self.repeatable)
150         xml += u"      <description>%s</description>\n" %  (self.description)
151         xml += u"    </subfield>\n"
152         return xml
153   
154 class Indicator(object):
155     """
156     Describes the properties of an indicator-value pair for a MARC field
157     """
158     def __init__(self, position, value, description):
159         self.position = position
160         self.value = value
161         self.description = description
162
163     def to_xml(self):
164         """
165         Convert the indicator-value pair to XML representation
166         """
167         xml = u"    <indicator position='%s' value='%s'>\n" % (self.position, self.value)
168         xml += u"      <description>%s</description>\n" %  (self.description)
169         xml += u"    </indicator>\n"
170         return xml
171  
172 def process_indicator(field, position, raw_ind):
173     """
174     Given an XML chunk holding indicator data,
175     append Indicator objects to a MARC field
176     """
177     if (re.compile(r'indicateur\s*-\s*Non').search(raw_ind.contents[0])):
178         return None
179     if (not raw_ind.ul):
180         print "No %d indicator for %s, although not not defined either..." % (position, field.tag)
181         return None
182     ind_values = raw_ind.ul.findAll('li')
183     for value in ind_values:
184         text = ''.join(value.findAll(text=True))
185         if (re.compile(u'non précisé').search(text)):
186             continue
187         matches = re.compile(r'^(\S(-\S)?)\s*-\s*(.+)$', re.S).search(text)
188         if matches is None: 
189             continue
190         new_ind = Indicator(position, matches.group(1), matches.group(3))
191         field.indicators.append(new_ind)
192
193 def process_subfield(field, subfield):
194     """
195     Given an XML chunk holding subfield data,
196     append a Subfield object to a MARC field
197     """
198     repeatable = 'true'
199
200     if (subfield.span):
201         if (re.compile(r'\(R\)').search(subfield.span.renderContents())):
202             repeatable = 'false'
203         subfield.span.extract()
204     elif (subfield.small):
205         if (re.compile(r'\(R\)').search(subfield.small.renderContents())):
206             repeatable = 'false'
207         subfield.small.extract()
208     else:
209         print "%s has no small or span tags?" % (field.tag)
210
211     subfield_text = re.compile(r'\n').sub(' ', ''.join(subfield.findAll(text=True)))
212     matches = re.compile(r'^\$(\w)\s*-\s*(.+)$', re.S).search(subfield_text)
213     if (not matches):
214         print "No subfield match for field: " + field.tag
215         return None
216     field.subfields.append(Subfield(matches.group(1), repeatable, matches.group(2)))
217
218 def process_tag(tag):
219     """
220     Given a chunk of XML representing a MARC field, generate a MarcField object
221     """
222     repeatable = 'true'
223     description = u''
224
225     # Get tag
226     tag_num = re.compile(r'^mrcb(\d+)').sub(r'\1', tag['id'])
227     if (len(tag_num) != 3):
228         return None
229
230     # Get repeatable - most stored in <span>, some stored in <small>
231     if (re.compile(r'\(NR\)').search(tag.renderContents())):
232         repeatable = 'false'
233
234     # Get description
235     desc = tag.parent.findNextSibling('p')
236     if (not desc):
237         print "No description for %s" % (tag_num)
238     else:
239         if (str(desc.__class__) == 'BeautifulSoup.Tag'):
240             try:
241                 description += u''.join(desc.findAll(text=True))
242             except:
243                 print "Bad description for: " + tag_num
244                 print u' '.join(desc.findAll(text=True))
245         else:
246             description += desc.string
247
248     # Create the tag
249     field = MarcField(tag_num, repeatable, description)
250
251     for desc in tag.parent.findNextSiblings():
252         if (str(desc.__class__) == 'BeautifulSoup.Tag'):
253             if (desc.name == 'h2'):
254                 break
255             elif (desc.name == 'h3' and re.compile(r'Indicateurs').search(desc.string)):
256                 # process indicators
257                 first_ind = desc.findNextSibling('ul').li
258                 second_ind = first_ind.findNextSibling('li')
259                 if (not second_ind):
260                     second_ind = first_ind.parent.findNextSibling('ul').li
261                 process_indicator(field, 1, first_ind)
262                 process_indicator(field, 2, second_ind)
263             elif (desc.name == 'h3' and re.compile(r'Codes de sous').search(desc.string)):
264                 # Get subfields
265                 subfield = desc.findNextSibling('ul').li
266                 while (subfield):
267                     process_subfield(field, subfield)
268                     subfield = subfield.findNextSibling('li')
269
270     return field
271
272 if __name__ == '__main__':
273     import copy
274     import os
275     import re
276     import subprocess
277
278     ALL_MY_FIELDS = MarcCollection()
279
280     # Run through the LAC-BAC MARC files we care about and convert like crazy   
281     for filename in os.listdir('.'):
282
283         if (not re.compile(r'^040010-1\d\d\d-f.html').search(filename)):
284             continue
285         print filename
286         devnull = open('/dev/null', 'w')
287         file = subprocess.Popen(
288             ('tidy', '-asxml', '-n', '-q', '-utf8', filename),
289             stdout=subprocess.PIPE, stderr=devnull).communicate()[0]
290
291         # Strip out the hard spaces on our way through
292         hardMassage = [(re.compile(r'&#160;'), lambda match: ' ')]
293         myHardMassage = copy.copy(BeautifulSoup.MARKUP_MASSAGE)
294         myHardMassage.extend(myHardMassage)
295
296         filexml = BeautifulSoup(file, markupMassage=myHardMassage)
297
298         tags = filexml.findAll('a', id=re.compile(r'^mrcb'))
299         for tag in tags:
300             field = process_tag(tag)
301             if (field):
302                 ALL_MY_FIELDS.add_field(field)
303
304     MARCOUT = open('marcedit-tooltips-fr.xml', 'w')
305     MARCOUT.write(ALL_MY_FIELDS.to_xml().encode('UTF-8'))
306     MARCOUT.close()