source: packages/ufo2fdk/branches/ufo3/Lib/ufo2fdk/makeotfParts.py @ 1141

Revision 1141, 27.9 KB checked in by tal, 15 months ago (diff)
Added some early support for source glyph names that are not legal according to the makeotf specs.
Line 
1import os
2import shutil
3import re
4from fontInfoData import getAttrWithFallback, intListToNum
5from outlineOTF import OutlineOTFCompiler
6from featureTableWriter import FeatureTableWriter, winStr, macStr
7from kernFeatureWriter import KernFeatureWriter
8try:
9    sorted
10except NameError:
11    def sorted(l):
12        l = list(l)
13        l.sort()
14        return l
15
16
17class MakeOTFPartsCompiler(object):
18
19    """
20    This object will create the "parts" needed by the FDK.
21    The only external method is :meth:`ufo2fdk.tools.makeotfParts.compile`.
22    There is one attribute, :attr:`ufo2fdk.tools.makeotfParts.path`
23    that may be referenced externally. That is a dictionary of
24    paths to the various parts.
25
26    When creating this object, you must provide a *font*
27    object and a *path* indicating where the parts should
28    be saved. Optionally, you can provide a *glyphOrder*
29    list of glyph names indicating the order of the glyphs
30    in the font. You may also provide an *outlineCompilerClass*
31    argument that will serve as the outline source compiler.
32    The class passed for this argument must be a subclass of
33    :class:`ufo2fdk.tools.outlineOTF.OutlineOTFCompiler`.
34    """
35
36    def __init__(self, font, path, glyphOrder=None, glyphDesignNameToFinalNameMap=None, outlineCompilerClass=OutlineOTFCompiler):
37        self.font = font
38        self.path = path
39        self.outlineCompilerClass = outlineCompilerClass
40        # store the glyph order
41        if glyphOrder is None:
42            glyphOrder = sorted(font.keys())
43        self.glyphOrder = glyphOrder
44        # set up the production names
45        self.glyphDesignNameToFinalNameMap = self.makeGlyphDesignNameToFinalNameMap(glyphDesignNameToFinalNameMap)
46        # make the paths for all files
47        self.paths = dict(
48            outlineSource=os.path.join(path, "font.otf"),
49            menuName=os.path.join(path, "menuname"),
50            glyphOrder=os.path.join(path, "glyphOrder"),
51            fontInfo=os.path.join(path, "fontinfo"),
52            features=os.path.join(path, "features")
53        )
54
55    def makeGlyphDesignNameToFinalNameMap(self, providedMap):
56        """
57        Make a map of {glyphName : makeotfLegalGlyphName}
58        for all gyphs in the glyph order..
59        """
60        finalMap = {}
61        if providedMap is not None:
62            finalMap.update(providedMap)
63        # gather glyphs that need a final name
64        needFinalName = [glyphName for glyphName in self.glyphOrder if glyphName not in finalMap]
65        # store names that don't need to be changed
66        for glyphName in needFinalName:
67            if isLegalGlyphName(glyphName):
68                finalMap[glyphName] = glyphName
69        # make names for the rest
70        for glyphName in needFinalName:
71            if glyphName in finalMap:
72                continue
73            uniValue = None
74            if glyphName in self.font:
75                uniValue = self.font[glyphName].unicode
76            finalMap[glyphName] = normalizeGlyphName(glyphName, uniValue, finalMap.values())
77        # done
78        return finalMap
79
80    def compile(self):
81        """
82        Compile the parts.
83        """
84        # set up the parts directory removing
85        # an existing directory if necessary.
86        if os.path.exists(self.path):
87            shutil.rmtree(self.path)
88        os.mkdir(self.path)
89        # build the parts
90        self.setupFile_outlineSource(self.paths["outlineSource"])
91        self.setupFile_menuName(self.paths["menuName"])
92        self.setupFile_glyphOrder(self.paths["glyphOrder"])
93        self.setupFile_fontInfo(self.paths["fontInfo"])
94        self.setupFile_features(self.paths["features"])
95
96    def setupFile_outlineSource(self, path):
97        """
98        Make the outline source file.
99
100        **This should not be called externally.** Subclasses
101        may override this method to handle the file creation
102        in a different way if desired.
103        """
104        c = self.outlineCompilerClass(self.font, path, self.glyphOrder)
105        c.compile()
106
107    def setupFile_menuName(self, path):
108        """
109        Make the menu name source file. This gets the values for
110        the file using the fallback system as described below:
111
112        ====  ===
113        [PS]  postscriptFontName
114        f=    openTypeNamePreferredFamilyName
115        s=    openTypeNamePreferredSubfamilyName
116        l=    styleMapFamilyName
117        m=1,  openTypeNameCompatibleFullName
118        ====  ===
119
120        **This should not be called externally.** Subclasses
121        may override this method to handle the file creation
122        in a different way if desired.
123        """
124        psName = getAttrWithFallback(self.font.info,"postscriptFontName")
125        lines = [
126            "[%s]" % psName
127        ]
128        # family name
129        familyName = getAttrWithFallback(self.font.info,"openTypeNamePreferredFamilyName")
130        encodedFamilyName = winStr(familyName)
131        lines.append("f=%s" % encodedFamilyName)
132        if encodedFamilyName != familyName:
133            lines.append("f=1,%s" % macStr(familyName))
134        # style name
135        styleName = getAttrWithFallback(self.font.info,"openTypeNamePreferredSubfamilyName")
136        encodedStyleName = winStr(styleName)
137        lines.append("s=%s" % encodedStyleName)
138        if encodedStyleName != styleName:
139            lines.append("s=1,%s" % macStr(styleName))
140        # compatible name
141        winCompatible = getAttrWithFallback(self.font.info,"styleMapFamilyName")
142        ## the second qualification here is in place for Mac Office <= 2004.
143        ## in that app the menu name is pulled from name ID 18. the font
144        ## may have standard naming data that combines to a length longer
145        ## than the app can handle (see Adobe Tech Note #5088). the designer
146        ## may have created a specific openTypeNameCompatibleFullName to
147        ## get around this problem. sigh, old app bugs live long lives.
148        if winCompatible != familyName or self.font.info.openTypeNameCompatibleFullName is not None:
149            # windows
150            l = "l=%s" % winCompatible
151            lines.append(l)
152            # mac
153            macCompatible = getAttrWithFallback(self.font.info,"openTypeNameCompatibleFullName")
154            l = "m=1,%s" % macCompatible
155            lines.append(l)
156        text = "\n".join(lines) + "\n"
157        f = open(path, "wb")
158        f.write(text)
159        f.close()
160
161    def setupFile_glyphOrder(self, path):
162        """
163        Make the glyph order source file.
164
165        **This should not be called externally.** Subclasses
166        may override this method to handle the file creation
167        in a different way if desired.
168        """
169        lines = []
170        for designName in self.glyphOrder:
171            finalName = self.glyphDesignNameToFinalNameMap[designName]
172            if designName in self.font and self.font[designName].unicode is not None:
173                code = self.font[designName].unicode
174                code = hex(code)[2:].upper()
175                if len(code) < 4:
176                    code = code.zfill(4)
177                line = "%s %s uni%s" % (finalName, glyphName, code)
178            else:
179                line = "%s %s" % (finalName, glyphName)
180            lines.append(line)
181        text = "\n".join(lines) + "\n"
182        f = open(path, "wb")
183        f.write(text)
184        f.close()
185
186    def setupFile_fontInfo(self, path):
187        """
188        Make the font info source file. This gets the values for
189        the file using the fallback system as described below:
190
191        ==========================  ===
192        IsItalicStyle               styleMapStyleName
193        IsBoldStyle                 styleMapStyleName
194        PreferOS/2TypoMetrics       openTypeOS2Selection
195        IsOS/2WidthWeigthSlopeOnly  openTypeOS2Selection
196        IsOS/2OBLIQUE               openTypeOS2Selection
197        ==========================  ===
198
199        **This should not be called externally.** Subclasses
200        may override this method to handle the file creation
201        in a different way if desired.
202        """
203        lines = []
204        # style mapping
205        styleMapStyleName = getAttrWithFallback(self.font.info,"styleMapStyleName")
206        if styleMapStyleName in ("italic", "bold italic"):
207            lines.append("IsItalicStyle true")
208        else:
209            lines.append("IsItalicStyle false")
210        if styleMapStyleName in ("bold", "bold italic"):
211            lines.append("IsBoldStyle true")
212        else:
213            lines.append("IsBoldStyle false")
214        # fsSelection bits
215        selection = getAttrWithFallback(self.font.info,"openTypeOS2Selection")
216        if 7 in selection:
217            lines.append("PreferOS/2TypoMetrics true")
218        else:
219            lines.append("PreferOS/2TypoMetrics false")
220        if 8 in selection:
221            lines.append("IsOS/2WidthWeigthSlopeOnly true")
222        else:
223            lines.append("IsOS/2WidthWeigthSlopeOnly false")
224        if 9 in selection:
225            lines.append("IsOS/2OBLIQUE true")
226        else:
227            lines.append("IsOS/2OBLIQUE false")
228        # write the file
229        if lines:
230            f = open(path, "wb")
231            f.write("\n".join(lines))
232            f.close()
233
234    def setupFile_features(self, path):
235        """
236        Make the features source file. If any tables
237        or the kern feature are defined in the font's
238        features, they will not be overwritten.
239
240        **This should not be called externally.** Subclasses
241        may override this method to handle the file creation
242        in a different way if desired.
243        """
244        # force absolute includes into the features
245        if self.font.path is None:
246            existingFeaturePath = None
247            existing = self.font.features.text
248            if existing is None:
249                existing = ""
250        else:
251            existingFeaturePath = os.path.join(self.font.path, "features.fea")
252            existing = forceAbsoluteIncludesInFeatures(self.font.features.text, os.path.dirname(self.font.path))
253        # break the features into parts
254        features, tables = extractFeaturesAndTables(existing, scannedFiles=[existingFeaturePath])
255        # build tables that are not in the existing features
256        autoTables = {}
257        if "head" not in tables:
258            autoTables["head"] = self.writeFeatures_head()
259        if "hhea" not in tables:
260            autoTables["hhea"] = self.writeFeatures_hhea()
261        if "OS/2" not in tables:
262            autoTables["OS/2"] = self.writeFeatures_OS2()
263        if "name" not in tables:
264            autoTables["name"] = self.writeFeatures_name()
265        # build the kern feature if necessary
266        autoFeatures = {}
267        if "kern" not in features and len(self.font.kerning):
268            autoFeatures["kern"] = self.writeFeatures_kern()
269        # write the features
270        features = [existing]
271        for name, text in sorted(autoFeatures.items()):
272            features.append(text)
273        for name, text in sorted(autoTables.items()):
274            features.append(text)
275        features = "\n\n".join(features)
276        # write the result
277        f = open(path, "wb")
278        f.write(features)
279        f.close()
280
281    def writeFeatures_kern(self):
282        """
283        Write the kern feature to a string and return it.
284
285        **This should not be called externally.** Subclasses
286        may override this method to handle the string creation
287        in a different way if desired.
288        """
289        writer = KernFeatureWriter(self.font)
290        return writer.write()
291
292    def writeFeatures_head(self):
293        """
294        Write the head to a string and return it.
295
296        This gets the values for the file using the fallback
297        system as described below:
298
299        =====  ===
300        X.XXX  versionMajor.versionMinor
301        =====  ===
302
303        **This should not be called externally.** Subclasses
304        may override this method to handle the string creation
305        in a different way if desired.
306        """
307        versionMajor = getAttrWithFallback(self.font.info, "versionMajor")
308        versionMinor = getAttrWithFallback(self.font.info, "versionMinor")
309        value = "%d.%s" % (versionMajor, str(versionMinor).zfill(3))
310        writer = FeatureTableWriter("head")
311        writer.addLineWithKeyValue("FontRevision", value)
312        return writer.write()
313
314    def writeFeatures_hhea(self):
315        """
316        Write the hhea to a string and return it.
317
318        This gets the values for the file using the fallback
319        system as described below:
320
321        ===========  ===
322        Ascender     openTypeHheaAscender
323        Descender    openTypeHheaDescender
324        LineGap      openTypeHheaLineGap
325        CaretOffset  openTypeHheaCaretOffset
326        ===========  ===
327
328        **This should not be called externally.** Subclasses
329        may override this method to handle the string creation
330        in a different way if desired.
331        """
332        ascender = getAttrWithFallback(self.font.info, "openTypeHheaAscender")
333        descender = getAttrWithFallback(self.font.info, "openTypeHheaDescender")
334        lineGap = getAttrWithFallback(self.font.info, "openTypeHheaLineGap")
335        caret = getAttrWithFallback(self.font.info, "openTypeHheaCaretOffset")
336        writer = FeatureTableWriter("hhea")
337        writer.addLineWithKeyValue("Ascender", _roundInt(ascender))
338        writer.addLineWithKeyValue("Descender", _roundInt(descender))
339        writer.addLineWithKeyValue("LineGap", _roundInt(lineGap))
340        writer.addLineWithKeyValue("CaretOffset", _roundInt(caret))
341        return writer.write()
342
343    def writeFeatures_name(self):
344        """
345        Write the name to a string and return it.
346
347        This gets the values for the file using the fallback
348        system as described below:
349
350        =========  ===
351        nameid 0   copyright
352        nameid 7   trademark
353        nameid 8   openTypeNameManufacturer
354        nameid 9   openTypeNameDesigner
355        nameid 10  openTypeNameDescription
356        nameid 11  openTypeNameManufacturerURL
357        nameid 12  openTypeNameDesignerURL
358        nameid 13  openTypeNameLicense
359        nameid 14  openTypeNameLicenseURL
360        nameid 19  openTypeNameSampleText
361        =========  ===
362
363        **This should not be called externally.** Subclasses
364        may override this method to handle the string creation
365        in a different way if desired.
366        """
367        idToAttr = [
368            (0  , "copyright"),
369            (7  , "trademark"),
370            (8  , "openTypeNameManufacturer"),
371            (9  , "openTypeNameDesigner"),
372            (10 , "openTypeNameDescription"),
373            (11 , "openTypeNameManufacturerURL"),
374            (12 , "openTypeNameDesignerURL"),
375            (13 , "openTypeNameLicense"),
376            (14 , "openTypeNameLicenseURL"),
377            (19 , "openTypeNameSampleText")
378        ]
379        multilineNameTableEntries = {}
380        lines = []
381        for id, attr in idToAttr:
382            value = getAttrWithFallback(self.font.info, attr)
383            if value is None:
384                continue
385            s = 'nameid %d "%s";' % (id, winStr(value))
386            lines.append(s)
387            s = 'nameid %d 1 "%s";' % (id, macStr(value))
388            lines.append(s)
389        if not lines:
390            return ""
391        writer = FeatureTableWriter("name")
392        for line in lines:
393            writer.addLine(line)
394        return writer.write()
395
396    def writeFeatures_OS2(self):
397        """
398        Write the OS/2 to a string and return it.
399
400        This gets the values for the file using the fallback
401        system as described below:
402
403        =============  ===
404        FSType         openTypeOS2Type
405        Panose         openTypeOS2Panose
406        UnicodeRange   openTypeOS2UnicodeRanges
407        CodePageRange  openTypeOS2CodePageRanges
408        TypoAscender   openTypeOS2TypoAscender
409        TypoDescender  openTypeOS2TypoDescender
410        TypoLineGap    openTypeOS2TypoLineGap
411        winAscent      openTypeOS2WinAscent
412        winDescent     openTypeOS2WinDescent
413        XHeight        xHeight
414        CapHeight      capHeight
415        WeightClass    openTypeOS2WeightClass
416        WidthClass     openTypeOS2WidthClass
417        Vendor         openTypeOS2VendorID
418        =============  ===
419
420        **This should not be called externally.** Subclasses
421        may override this method to handle the string creation
422        in a different way if desired.
423        """
424        codePageBitTranslation = {
425            0  : "1252",
426            1  : "1250",
427            2  : "1251",
428            3  : "1253",
429            4  : "1254",
430            5  : "1255",
431            6  : "1256",
432            7  : "1257",
433            8  : "1258",
434            16 : "874",
435            17 : "932",
436            18 : "936",
437            19 : "949",
438            20 : "950",
439            21 : "1361",
440            48 : "869",
441            49 : "866",
442            50 : "865",
443            51 : "864",
444            52 : "863",
445            53 : "862",
446            54 : "861",
447            55 : "860",
448            56 : "857",
449            57 : "855",
450            58 : "852",
451            59 : "775",
452            60 : "737",
453            61 : "708",
454            62 : "850",
455            63 : "437"
456        }
457        # writer
458        writer = FeatureTableWriter("OS/2")
459        # type
460        writer.addLineWithKeyValue("FSType", intListToNum(getAttrWithFallback(self.font.info, "openTypeOS2Type"), 0, 16))
461        # panose
462        panose = [str(i) for i in getAttrWithFallback(self.font.info, "openTypeOS2Panose")]
463        writer.addLineWithKeyValue("Panose", " ".join(panose))
464        # unicode ranges
465        unicodeRange = [str(i) for i in getAttrWithFallback(self.font.info, "openTypeOS2UnicodeRanges")]
466        if unicodeRange:
467            writer.addLineWithKeyValue("UnicodeRange", " ".join(unicodeRange))
468        # code page ranges
469        codePageRange = [codePageBitTranslation[i] for i in getAttrWithFallback(self.font.info, "openTypeOS2CodePageRanges") if i in codePageBitTranslation]
470        if codePageRange:
471            writer.addLineWithKeyValue("CodePageRange", " ".join(codePageRange))
472        # vertical metrics
473        writer.addLineWithKeyValue("TypoAscender", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoAscender")))
474        writer.addLineWithKeyValue("TypoDescender", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoDescender")))
475        writer.addLineWithKeyValue("TypoLineGap", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoLineGap")))
476        writer.addLineWithKeyValue("winAscent", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2WinAscent")))
477        writer.addLineWithKeyValue("winDescent", abs(_roundInt(getAttrWithFallback(self.font.info, "openTypeOS2WinDescent"))))
478        writer.addLineWithKeyValue("XHeight", _roundInt(getAttrWithFallback(self.font.info, "xHeight")))
479        writer.addLineWithKeyValue("CapHeight", _roundInt(getAttrWithFallback(self.font.info, "capHeight")))
480        writer.addLineWithKeyValue("WeightClass", getAttrWithFallback(self.font.info, "openTypeOS2WeightClass"))
481        writer.addLineWithKeyValue("WidthClass", getAttrWithFallback(self.font.info, "openTypeOS2WidthClass"))
482        writer.addLineWithKeyValue("Vendor", '"%s"' % getAttrWithFallback(self.font.info, "openTypeOS2VendorID"))
483        return writer.write()
484
485
486# -----------
487# Glyph Names
488# -----------
489
490import unicodedata
491
492_digits = set("0123456789")
493_validCharacters = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.")
494
495def isLegalGlyphName(glyphName):
496    """
497    >>> isLegalGlyphName("a")
498    True
499    >>> isLegalGlyphName(".foo")
500    False
501    >>> isLegalGlyphName(".notdef")
502    True
503    >>> isLegalGlyphName("foo.bar")
504    True
505    >>> isLegalGlyphName("1foo")
506    False
507    >>> isLegalGlyphName("foo1")
508    True
509    >>> isLegalGlyphName("f*o")
510    False
511    >>> isLegalGlyphName("abcdefghijklmnopqrstuvwxyz01234")
512    True
513    >>> isLegalGlyphName("abcdefghijklmnopqrstuvwxyz012345")
514    False
515    """
516    # must not start with a digit or period
517    if glyphName[0] in _digits:
518        return False
519    if glyphName[0] == "." and glyphName != ".notdef":
520        return False
521    # up to 31 characters in length
522    if len(glyphName) > 31:
523        return False
524    # must be entirely comprised of characters from A-Z a-z 0-9 . _
525    for character in glyphName:
526        if character not in _validCharacters:
527            return False
528    # passed
529    return True
530
531def normalizeGlyphName(glyphName, uniValue, existing):
532    """
533    >>> normalizeGlyphName("a-b-c", None, [])
534    'abc'
535    >>> normalizeGlyphName("a-b-c", None, ["abc"])
536    'abc.1'
537    >>> normalizeGlyphName("!", int("0021", 16), [])
538    'uni0021'
539    >>> normalizeGlyphName("!", int("0021", 16), ['uni0021'])
540    'uni0021.1'
541    >>> normalizeGlyphName("abcdefghijklmnopqrstuvwxyz012345", None, [])
542    'glyph1'
543    >>> normalizeGlyphName("a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-0-1-2-3-4-5", None, [])
544    'glyph1'
545    """
546    # convert to unicode
547    glyphName = unicode(glyphName)
548    # remove illegal characters
549    glyphName = unicodedata.normalize("NFKD", glyphName)
550    glyphName = glyphName.encode("ascii", "ignore")
551    glyphName = "".join([c for c in glyphName if c in _validCharacters])
552    # no new name
553    if not glyphName:
554        # quickly try to apply the adobe standard
555        if uniValue:
556            if uniValue > 0x0000 and uniValue < 0xFFFF:
557                prefix = "uni"
558            else:
559                prefix = "u"
560            glyphName = prefix + hex(uniValue)[2:].zfill(4).upper()
561    # hit test
562    if glyphName in existing:
563        glyphName = _makeUniqueGlyphName(glyphName, existing)
564    # test for validity
565    if not isLegalGlyphName(glyphName):
566        glyphName = None
567    # fallback
568    if not glyphName:
569        glyphName = _makeUniqueFallbackGlyphName(existing)
570    return glyphName
571
572def _makeUniqueGlyphName(glyphName, existing, number=1):
573    """
574    _makeUniqueGlyphName("abc", ["abc"])
575    'abc.1'
576    """
577    newName = "%s.%d" % (glyphName, number)
578    if newName in existing:
579        return _makeUniqueGlyphName(glyphName, existing, number+1)
580    return newName
581
582def _makeUniqueFallbackGlyphName(existing, number=1):
583    """
584    >>> _makeUniqueFallbackGlyphName([])
585    'glyph1'
586    >>> _makeUniqueFallbackGlyphName(["glyph1"])
587    'glyph2'
588    """
589    assert number < 100000 # arbitrary, but come on. 100,000 illegal glyph names?
590    name = "glyph%d" % number
591    if name in existing:
592        return _makeUniqueFallbackGlyphName(existing, number+1)
593    return name
594
595# --------
596# Features
597# --------
598
599includeRE = re.compile(
600    "include"
601    "\s*"
602    "\("
603    "([^\)]+)"
604    "\)"
605    )
606
607def forceAbsoluteIncludesInFeatures(text, directory):
608    """
609    Convert relative includes in the *text*
610    to absolute includes.
611    """
612    for includePath in includeRE.findall(text):
613        currentDirectory = directory
614        parts = includePath.split("/")
615        for index, part in enumerate(parts):
616            part = part.strip()
617            if not part:
618                continue
619            if part == "..":
620                currentDirectory = os.path.dirname(currentDirectory)
621            elif part == ".":
622                continue
623            else:
624                break
625        subPath = "/".join(parts[index:])
626        srcPath = os.path.join(currentDirectory, subPath)
627        text = text.replace(includePath, srcPath)
628    return text
629
630def _roundInt(value):
631    return int(round(value))
632
633# ----------------------
634# Basic Feature Splitter
635# ----------------------
636
637stringRE = re.compile(
638    "(\"[^$\"]*\")"
639)
640featureTableStartRE = re.compile(
641    "("
642    "feature"
643    "\s+"
644    "\S{4}"
645    "\s*"
646    "\{"
647    "|"
648    "table"
649    "\s+"
650    "\S{4}"
651    "\s*"
652    "\{"
653    ")",
654    re.MULTILINE
655)
656featureNameRE = re.compile(
657    "feature"
658    "\s+"
659    "(\S{4})"
660    "\s*"
661    "\{"
662)
663tableNameRE = re.compile(
664    "table"
665    "\s+"
666    "(\S{4})"
667    "\s*"
668    "\{"
669)
670
671def extractFeaturesAndTables(text, scannedFiles=[]):
672    # strip all comments
673    decommentedLines = [line.split("#")[0] for line in text.splitlines()]
674    text = "\n".join(decommentedLines)
675    # replace all strings with temporary placeholders.
676    destringedLines = []
677    stringReplacements = {}
678    for line in text.splitlines():
679        if "\"" in line:
680            line = line.replace("\\\"", "__ufo2fdk_temp_escaped_quote__")
681            for found in stringRE.findall(line):
682                temp = "__ufo2fdk_temp_string_%d__" % len(stringReplacements)
683                line = line.replace(found, temp, 1)
684                stringReplacements[temp] = found.replace("__ufo2fdk_temp_escaped_quote__", "\\\"")
685            line = line.replace("__ufo2fdk_temp_escaped_quote__", "\\\"")
686        destringedLines.append(line)
687    text = "\n".join(destringedLines)
688    # extract all includes
689    includes = includeRE.findall(text)
690    # slice off the text that comes before
691    # the first feature/table definition
692    precedingText = ""
693    startMatch = featureTableStartRE.search(text)
694    if startMatch is not None:
695        start, end = startMatch.span()
696        precedingText = text[:start].strip()
697        text = text[start:]
698    else:
699        precedingText = text
700        text = ""
701    # break the features
702    broken = _textBreakRecurse(text)
703    # organize into tables and features
704    features = {}
705    tables = {}
706    for text in broken:
707        text = text.strip()
708        if not text:
709            continue
710        # replace the strings
711        finalText = text
712        for temp, original in stringReplacements.items():
713            if temp in finalText:
714                del stringReplacements[temp]
715                finalText = finalText.replace(temp, original, 1)
716        finalText = finalText.strip()
717        # grab feature or table names and store
718        featureMatch = featureNameRE.search(text)
719        if featureMatch is not None:
720            features[featureMatch.group(1)] = finalText
721        else:
722            tableMatch = tableNameRE.search(text)
723            tables[tableMatch.group(1)] = finalText
724    # scan all includes
725    for path in includes:
726        if path in scannedFiles:
727            continue
728        scannedFiles.append(path)
729        if os.path.exists(path):
730            f = open(path, "r")
731            text = f.read()
732            f.close()
733            f, t = extractFeaturesAndTables(text, scannedFiles)
734            features.update(f)
735            tables.update(t)
736    return features, tables
737
738def _textBreakRecurse(text):
739    matched = []
740    match = featureTableStartRE.search(text)
741    if match is None:
742        matched.append(text)
743    else:
744        start, end = match.span()
745        # add any preceding text to the previous item
746        if start != 0:
747            precedingText = matched.pop(0)
748            precedingText += text[:start]
749            matched.insert(0, precedingText)
750        # look ahead to see if there is another feature
751        next = text[end:]
752        nextMatch = featureTableStartRE.search(next)
753        if nextMatch is None:
754            # if nothing has been found, add
755            # the remaining text to the feature
756            matchedText = text[start:]
757            matched.append(matchedText)
758        else:
759            # if one has been found, grab all text
760            # from before the feature start and add
761            # it to the current feature.
762            nextStart, nextEnd = nextMatch.span()
763            matchedText = text[:end + nextStart]
764            matched.append(matchedText)
765            # recurse through the remaining text
766            matched += _textBreakRecurse(next[nextStart:])
767    return matched
768
769
770extractFeaturesAndTablesTestText = """
771@foo = [bar];
772
773# test commented item
774#feature fts1 {
775#    sub foo by bar;
776#} fts1;
777
778feature fts2 {
779    sub foo by bar;
780} fts2;
781
782table tts1 {
783    nameid 1 "feature this { is not really a \\\"feature that { other thing is";
784} tts1;feature fts3 { sub a by b;} fts3;
785"""
786
787extractFeaturesAndTablesTestResult = (
788    {
789        'fts2': 'feature fts2 {\n    sub foo by bar;\n} fts2;',
790        'fts3': 'feature fts3 { sub a by b;} fts3;'
791    },
792    {
793        'tts1': 'table tts1 {\n    nameid 1 "feature this { is not really a \\"feature that { other thing is";\n} tts1;'
794    }
795)
796
797def testBreakFeaturesAndTables():
798    """
799    >>> r = extractFeaturesAndTables(extractFeaturesAndTablesTestText)
800    >>> r == extractFeaturesAndTablesTestResult
801    True
802    """
803
804if __name__ == "__main__":
805    import doctest
806    doctest.testmod()
Note: See TracBrowser for help on using the repository browser.