├── .gitignore
├── pystache
├── commands
│ ├── __init__.py
│ ├── test.py
│ └── render.py
├── __init__.py
├── init.py
├── LICENSE
├── parsed.py
├── template_spec.py
├── defaults.py
├── common.py
├── specloader.py
├── locator.py
├── loader.py
├── renderengine.py
├── README.md
├── context.py
├── parser.py
└── renderer.py
├── templates
├── fromleft-text.gif
├── appearing-text.gif
├── descending-text.gif
├── revealing-text.gif
├── appearing-text.xml
├── appearing-text.sif
├── descending-text.xml
├── fromleft-text.xml
├── descending-text.sif
├── fromleft-text.sif
├── revealing-text.xml
└── revealing-text.sif
├── test
├── appearing-text_result.gif
├── fromleft-text_result.gif
├── revealing-text_result.gif
├── descending-text_result.gif
├── keyframe-import-test_v1.0.sifz
├── labels3.txt
├── labels2.txt
├── labels-multi.txt
├── labels.txt
├── out-commandline.sif
├── keyframe-import-test_out-with-no-ends.sif
├── keyframe-import-test_out-with-overwrite.sif
├── keyframe-import-test_out-with-ends.sif
├── keyframe-import-test_v0.61.sif
├── keyframe-import-test_v1.0.sif
└── appearing-text_result.sif
├── plugin.xml
├── settings.py
├── README.md
├── synfig-import-labels.py
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/pystache/commands/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TODO: add a docstring.
3 |
4 | """
5 |
--------------------------------------------------------------------------------
/templates/fromleft-text.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/templates/fromleft-text.gif
--------------------------------------------------------------------------------
/templates/appearing-text.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/templates/appearing-text.gif
--------------------------------------------------------------------------------
/templates/descending-text.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/templates/descending-text.gif
--------------------------------------------------------------------------------
/templates/revealing-text.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/templates/revealing-text.gif
--------------------------------------------------------------------------------
/test/appearing-text_result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/test/appearing-text_result.gif
--------------------------------------------------------------------------------
/test/fromleft-text_result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/test/fromleft-text_result.gif
--------------------------------------------------------------------------------
/test/revealing-text_result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/test/revealing-text_result.gif
--------------------------------------------------------------------------------
/test/descending-text_result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/test/descending-text_result.gif
--------------------------------------------------------------------------------
/test/keyframe-import-test_v1.0.sifz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berteh/synfig-import-labels/HEAD/test/keyframe-import-test_v1.0.sifz
--------------------------------------------------------------------------------
/test/labels3.txt:
--------------------------------------------------------------------------------
1 | 0,100000 0,900000 short text
2 | 1,400000 2,871346 some longer text
3 | 1,966796 3,432578 again some loooong text
4 | 4,307196 4,807196 another short
5 |
--------------------------------------------------------------------------------
/test/labels2.txt:
--------------------------------------------------------------------------------
1 | 0,000000 0,900000 part one
2 | 1,000000 1,871346 part two
3 | 1,966796 2,432578 part three
4 | 2,507196 2,807196 part four
5 | 3,232578 3,712881 part five
6 | 4,255000 4,355000 part six
7 |
--------------------------------------------------------------------------------
/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Import Labels and Timings
4 | Importer Textes et Temps Clés
5 | synfig-import-labels.py
6 |
--------------------------------------------------------------------------------
/test/labels-multi.txt:
--------------------------------------------------------------------------------
1 | start stop text template
2 | 0,000000 0,900000 part one
3 | 1,000000 1,871346 part two descending-text
4 | 1,966796 2,432578 part three fromleft-text
5 | 2,507196 2,807196 part four
6 | 3,232578 3,712881 part five revealing-text
7 | 4,255000 4,355000 part six
--------------------------------------------------------------------------------
/test/labels.txt:
--------------------------------------------------------------------------------
1 | 0,100000 0,900000 part 1
2 | 0,400000 0,871346 highlight in part 1
3 | 0,966796 1,432578 down in part 1
4 | 1,307196 1,807196 some instantaneous event
5 | 1,532578 1,712881 check for overwrite
6 | 3,255000 3,355000 check for minutes
7 | 3,050000 3,749000 check for hours
8 |
--------------------------------------------------------------------------------
/pystache/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | TODO: add a docstring.
4 |
5 | """
6 |
7 | # We keep all initialization code in a separate module.
8 |
9 | from pystache.init import parse, render, Renderer, TemplateSpec
10 |
11 | __all__ = ['parse', 'render', 'Renderer', 'TemplateSpec']
12 |
13 | __version__ = '0.5.4' # Also change in setup.py.
14 |
--------------------------------------------------------------------------------
/pystache/commands/test.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides a command to test pystache (unit tests, doctests, etc).
5 |
6 | """
7 |
8 | import sys
9 |
10 | from pystache.tests.main import main as run_tests
11 |
12 |
13 | def main(sys_argv=sys.argv):
14 | run_tests(sys_argv=sys_argv)
15 |
16 |
17 | if __name__=='__main__':
18 | main()
19 |
--------------------------------------------------------------------------------
/pystache/init.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """
4 | This module contains the initialization logic called by __init__.py.
5 |
6 | """
7 |
8 | from pystache.parser import parse
9 | from pystache.renderer import Renderer
10 | from pystache.template_spec import TemplateSpec
11 |
12 |
13 | def render(template, context=None, **kwargs):
14 | """
15 | Return the given template string rendered using the given context.
16 |
17 | """
18 | renderer = Renderer()
19 | return renderer.render(template, context, **kwargs)
20 |
--------------------------------------------------------------------------------
/test/out-commandline.sif:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/test/keyframe-import-test_out-with-no-ends.sif:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/test/keyframe-import-test_out-with-overwrite.sif:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/test/keyframe-import-test_out-with-ends.sif:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/pystache/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2012 Chris Jerdonek. All rights reserved.
2 |
3 | Copyright (c) 2009 Chris Wanstrath
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/pystache/parsed.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Exposes a class that represents a parsed (or compiled) template.
5 |
6 | """
7 |
8 |
9 | class ParsedTemplate(object):
10 |
11 | """
12 | Represents a parsed or compiled template.
13 |
14 | An instance wraps a list of unicode strings and node objects. A node
15 | object must have a `render(engine, stack)` method that accepts a
16 | RenderEngine instance and a ContextStack instance and returns a unicode
17 | string.
18 |
19 | """
20 |
21 | def __init__(self):
22 | self._parse_tree = []
23 |
24 | def __repr__(self):
25 | return repr(self._parse_tree)
26 |
27 | def add(self, node):
28 | """
29 | Arguments:
30 |
31 | node: a unicode string or node object instance. See the class
32 | docstring for information.
33 |
34 | """
35 | self._parse_tree.append(node)
36 |
37 | def render(self, engine, context):
38 | """
39 | Returns: a string of type unicode.
40 |
41 | """
42 | # We avoid use of the ternary operator for Python 2.4 support.
43 | def get_unicode(node):
44 | if type(node) is str:
45 | return node
46 | return node.render(engine, context)
47 | parts = list(map(get_unicode, self._parse_tree))
48 | s = ''.join(parts)
49 |
50 | return str(s)
51 |
--------------------------------------------------------------------------------
/pystache/template_spec.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Provides a class to customize template information on a per-view basis.
5 |
6 | To customize template properties for a particular view, create that view
7 | from a class that subclasses TemplateSpec. The "spec" in TemplateSpec
8 | stands for "special" or "specified" template information.
9 |
10 | """
11 |
12 | class TemplateSpec(object):
13 |
14 | """
15 | A mixin or interface for specifying custom template information.
16 |
17 | The "spec" in TemplateSpec can be taken to mean that the template
18 | information is either "specified" or "special."
19 |
20 | A view should subclass this class only if customized template loading
21 | is needed. The following attributes allow one to customize/override
22 | template information on a per view basis. A None value means to use
23 | default behavior for that value and perform no customization. All
24 | attributes are initialized to None.
25 |
26 | Attributes:
27 |
28 | template: the template as a string.
29 |
30 | template_encoding: the encoding used by the template.
31 |
32 | template_extension: the template file extension. Defaults to "mustache".
33 | Pass False for no extension (i.e. extensionless template files).
34 |
35 | template_name: the name of the template.
36 |
37 | template_path: absolute path to the template.
38 |
39 | template_rel_directory: the directory containing the template file,
40 | relative to the directory containing the module defining the class.
41 |
42 | template_rel_path: the path to the template file, relative to the
43 | directory containing the module defining the class.
44 |
45 | """
46 |
47 | template = None
48 | template_encoding = None
49 | template_extension = None
50 | template_name = None
51 | template_path = None
52 | template_rel_directory = None
53 | template_rel_path = None
54 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | # configuration for keyframes import
2 | #
3 | LABELS_FILE = "labels.txt" # audacity labels file name, must be located in your synfig project directory
4 | IMPORT_START = True # set to True to import keyframe for start of label
5 | IMPORT_END = False # set to True to import keyframe for end of label
6 | START_SUFFIX = "" # suffix to add to a label-start keyframe, to distinguish it from label-end frame
7 | END_SUFFIX = " - end" # suffix to add to a label-end keyframe, to distinguish it from label-start frame
8 | OVERWRITE_KEYFRAMES_WITH_SAME_NAME = False # set to True to replace keyframe with exact same description
9 | GENERATE_OBJECTS = True # set to True to generate objects (such as text layers) for each label
10 | #
11 | # settings below only matter to object generation. don't bother if GENERATE_OBJECTS is False.
12 | TEMPLATE_NAME = "appearing-text" # the name of template you want to use. must be located in templates/ subdirectory, with .xml extension. default is "popping-text"
13 | SPLIT_WORDS = False # split each word in a separate object
14 | WAYPOINT_TYPE = "halt" # one of: constant, auto, linear, clamped, halt
15 | RANDOM_ORIGIN = 70 # set to a percentage [0-100] to randomize the object origin in the whole document viewbox (0 will stack them all at [0,0])
16 | ANIMATION_INTERVAL = 0.5 # interval (before and after the label time) used for (in & out) transition, in seconds. default is 0.5
17 | #
18 | ####################
19 | #
20 | # changing the values behind this point is not recommended as it may break the examples provided in the templates/ directory...
21 | # but if you still do, then you know what you're doing ;)
22 | #
23 | # values for "loop" based transitions (appearing,...)
24 | LOOP_BEFORE = "0.0"
25 | LOOP_MIDDLE = "1.0"
26 | LOOP_AFTER = "0.3"
27 | # values for "progressions" transitions (descending, revealing, fromleft...)
28 | VALUE_BEFORE = "-1.0"
29 | VALUE_MIDDLE = "0.0"
30 | VALUE_AFTER = "1.0"
31 | # location for plugin templates
32 | TEMPLATE_DIR = "templates"
33 | # labels parsing
34 | TSV_SEPARATOR = "\t" # single character separating data fields in LABELS_FILE
35 | HEADER = ["start","stop","text","template"]
--------------------------------------------------------------------------------
/pystache/defaults.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides a central location for defining default behavior.
5 |
6 | Throughout the package, these defaults take effect only when the user
7 | does not otherwise specify a value.
8 |
9 | """
10 |
11 | try:
12 | # Python 3.2 adds html.escape() and deprecates cgi.escape().
13 | from html import escape
14 | except ImportError:
15 | from cgi import escape
16 |
17 | import os
18 | import sys
19 |
20 | from pystache.common import MissingTags
21 |
22 |
23 | # How to handle encoding errors when decoding strings from str to unicode.
24 | #
25 | # This value is passed as the "errors" argument to Python's built-in
26 | # unicode() function:
27 | #
28 | # http://docs.python.org/library/functions.html#unicode
29 | #
30 | DECODE_ERRORS = 'strict'
31 |
32 | # The name of the encoding to use when converting to unicode any strings of
33 | # type str encountered during the rendering process.
34 | STRING_ENCODING = sys.getdefaultencoding()
35 |
36 | # The name of the encoding to use when converting file contents to unicode.
37 | # This default takes precedence over the STRING_ENCODING default for
38 | # strings that arise from files.
39 | FILE_ENCODING = sys.getdefaultencoding()
40 |
41 | # The delimiters to start with when parsing.
42 | DELIMITERS = ('{{', '}}')
43 |
44 | # How to handle missing tags when rendering a template.
45 | MISSING_TAGS = MissingTags.ignore
46 |
47 | # The starting list of directories in which to search for templates when
48 | # loading a template by file name.
49 | SEARCH_DIRS = [os.curdir] # i.e. ['.']
50 |
51 | # The escape function to apply to strings that require escaping when
52 | # rendering templates (e.g. for tags enclosed in double braces).
53 | # Only unicode strings will be passed to this function.
54 | #
55 | # The quote=True argument causes double but not single quotes to be escaped
56 | # in Python 3.1 and earlier, and both double and single quotes to be
57 | # escaped in Python 3.2 and later:
58 | #
59 | # http://docs.python.org/library/cgi.html#cgi.escape
60 | # http://docs.python.org/dev/library/html.html#html.escape
61 | #
62 | TAG_ESCAPE = lambda u: escape(u, quote=True)
63 |
64 | # The default template extension, without the leading dot.
65 | TEMPLATE_EXTENSION = 'mustache'
66 |
--------------------------------------------------------------------------------
/pystache/common.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Exposes functionality needed throughout the project.
5 |
6 | """
7 |
8 | from sys import version_info
9 |
10 | def _get_string_types():
11 | # TODO: come up with a better solution for this. One of the issues here
12 | # is that in Python 3 there is no common base class for unicode strings
13 | # and byte strings, and 2to3 seems to convert all of "str", "unicode",
14 | # and "basestring" to Python 3's "str".
15 | if version_info < (3, ):
16 | return str
17 | # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
18 | return (str, type("a".encode('utf-8')))
19 |
20 |
21 | _STRING_TYPES = _get_string_types()
22 |
23 |
24 | def is_string(obj):
25 | """
26 | Return whether the given object is a byte string or unicode string.
27 |
28 | This function is provided for compatibility with both Python 2 and 3
29 | when using 2to3.
30 |
31 | """
32 | return isinstance(obj, _STRING_TYPES)
33 |
34 |
35 | # This function was designed to be portable across Python versions -- both
36 | # with older versions and with Python 3 after applying 2to3.
37 | def read(path):
38 | """
39 | Return the contents of a text file as a byte string.
40 |
41 | """
42 | # Opening in binary mode is necessary for compatibility across Python
43 | # 2 and 3. In both Python 2 and 3, open() defaults to opening files in
44 | # text mode. However, in Python 2, open() returns file objects whose
45 | # read() method returns byte strings (strings of type `str` in Python 2),
46 | # whereas in Python 3, the file object returns unicode strings (strings
47 | # of type `str` in Python 3).
48 | f = open(path, 'rb')
49 | # We avoid use of the with keyword for Python 2.4 support.
50 | try:
51 | return f.read()
52 | finally:
53 | f.close()
54 |
55 |
56 | class MissingTags(object):
57 |
58 | """Contains the valid values for Renderer.missing_tags."""
59 |
60 | ignore = 'ignore'
61 | strict = 'strict'
62 |
63 |
64 | class PystacheError(Exception):
65 | """Base class for Pystache exceptions."""
66 | pass
67 |
68 |
69 | class TemplateNotFoundError(PystacheError):
70 | """An exception raised when a template is not found."""
71 | pass
72 |
--------------------------------------------------------------------------------
/templates/appearing-text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{text}}
26 |
27 |
28 |
29 | 0.300000
30 | 0.000000
31 | 0.000000
32 | 1.000000
33 |
34 |
35 |
36 | Sans Serif
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 0.2500000000
53 | 0.2500000000
54 |
55 |
56 |
57 |
58 | 0.5000000000
59 | 0.5000000000
60 |
61 |
62 |
63 |
64 | {{origin_x}}
65 | {{origin_y}}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/pystache/commands/render.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides command-line access to pystache.
5 |
6 | Run this script using the -h option for command-line help.
7 |
8 | """
9 |
10 |
11 | try:
12 | import json
13 | except:
14 | # The json module is new in Python 2.6, whereas simplejson is
15 | # compatible with earlier versions.
16 | try:
17 | import simplejson as json
18 | except ImportError:
19 | # Raise an error with a type different from ImportError as a hack around
20 | # this issue:
21 | # http://bugs.python.org/issue7559
22 | from sys import exc_info
23 | ex_type, ex_value, tb = exc_info()
24 | new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
25 | raise new_ex.__class__(new_ex).with_traceback(tb)
26 |
27 | # The optparse module is deprecated in Python 2.7 in favor of argparse.
28 | # However, argparse is not available in Python 2.6 and earlier.
29 | from optparse import OptionParser
30 | import sys
31 |
32 | # We use absolute imports here to allow use of this script from its
33 | # location in source control (e.g. for development purposes).
34 | # Otherwise, the following error occurs:
35 | #
36 | # ValueError: Attempted relative import in non-package
37 | #
38 | from pystache.common import TemplateNotFoundError
39 | from pystache.renderer import Renderer
40 |
41 |
42 | USAGE = """\
43 | %prog [-h] template context
44 |
45 | Render a mustache template with the given context.
46 |
47 | positional arguments:
48 | template A filename or template string.
49 | context A filename or JSON string."""
50 |
51 |
52 | def parse_args(sys_argv, usage):
53 | """
54 | Return an OptionParser for the script.
55 |
56 | """
57 | args = sys_argv[1:]
58 |
59 | parser = OptionParser(usage=usage)
60 | options, args = parser.parse_args(args)
61 |
62 | template, context = args
63 |
64 | return template, context
65 |
66 |
67 | # TODO: verify whether the setup() method's entry_points argument
68 | # supports passing arguments to main:
69 | #
70 | # http://packages.python.org/distribute/setuptools.html#automatic-script-creation
71 | #
72 | def main(sys_argv=sys.argv):
73 | template, context = parse_args(sys_argv, USAGE)
74 |
75 | if template.endswith('.mustache'):
76 | template = template[:-9]
77 |
78 | renderer = Renderer()
79 |
80 | try:
81 | template = renderer.load_template(template)
82 | except TemplateNotFoundError:
83 | pass
84 |
85 | try:
86 | context = json.load(open(context))
87 | except IOError:
88 | context = json.loads(context)
89 |
90 | rendered = renderer.render(template, context)
91 | print(rendered)
92 |
93 |
94 | if __name__=='__main__':
95 | main()
96 |
--------------------------------------------------------------------------------
/pystache/specloader.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module supports customized (aka special or specified) template loading.
5 |
6 | """
7 |
8 | import os.path
9 |
10 | from pystache.loader import Loader
11 |
12 |
13 | # TODO: add test cases for this class.
14 | class SpecLoader(object):
15 |
16 | """
17 | Supports loading custom-specified templates (from TemplateSpec instances).
18 |
19 | """
20 |
21 | def __init__(self, loader=None):
22 | if loader is None:
23 | loader = Loader()
24 |
25 | self.loader = loader
26 |
27 | def _find_relative(self, spec):
28 | """
29 | Return the path to the template as a relative (dir, file_name) pair.
30 |
31 | The directory returned is relative to the directory containing the
32 | class definition of the given object. The method returns None for
33 | this directory if the directory is unknown without first searching
34 | the search directories.
35 |
36 | """
37 | if spec.template_rel_path is not None:
38 | return os.path.split(spec.template_rel_path)
39 | # Otherwise, determine the file name separately.
40 |
41 | locator = self.loader._make_locator()
42 |
43 | # We do not use the ternary operator for Python 2.4 support.
44 | if spec.template_name is not None:
45 | template_name = spec.template_name
46 | else:
47 | template_name = locator.make_template_name(spec)
48 |
49 | file_name = locator.make_file_name(template_name, spec.template_extension)
50 |
51 | return (spec.template_rel_directory, file_name)
52 |
53 | def _find(self, spec):
54 | """
55 | Find and return the path to the template associated to the instance.
56 |
57 | """
58 | if spec.template_path is not None:
59 | return spec.template_path
60 |
61 | dir_path, file_name = self._find_relative(spec)
62 |
63 | locator = self.loader._make_locator()
64 |
65 | if dir_path is None:
66 | # Then we need to search for the path.
67 | path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name)
68 | else:
69 | obj_dir = locator.get_object_directory(spec)
70 | path = os.path.join(obj_dir, dir_path, file_name)
71 |
72 | return path
73 |
74 | def load(self, spec):
75 | """
76 | Find and return the template associated to a TemplateSpec instance.
77 |
78 | Returns the template as a unicode string.
79 |
80 | Arguments:
81 |
82 | spec: a TemplateSpec instance.
83 |
84 | """
85 | if spec.template is not None:
86 | return self.loader.str(spec.template, spec.template_encoding)
87 |
88 | path = self._find(spec)
89 |
90 | return self.loader.read(path, spec.template_encoding)
91 |
--------------------------------------------------------------------------------
/templates/appearing-text.sif:
--------------------------------------------------------------------------------
1 |
2 |
93 |
--------------------------------------------------------------------------------
/templates/descending-text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{text}}
13 |
14 |
15 |
16 | 0.033015
17 | 0.015800
18 | 0.203155
19 | 1.000000
20 |
21 |
22 |
23 | Sans Serif
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 0.3333333433
42 | 0.3333333433
43 |
44 |
45 |
46 |
47 |
48 |
49 | -0.5000000000
50 | -0.5000000000
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 0.5000000000
92 | 0.5000000000
93 |
94 |
95 |
96 |
97 |
98 |
99 | 0.0000000000
100 | 0.0000000000
101 |
102 |
103 |
104 |
105 | 0.0000000000
106 | -2.5000000000
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/templates/fromleft-text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 0.0000000000
14 | {{origin_y}}
15 |
16 |
17 |
18 |
19 |
20 |
21 | 0.0000000000
22 | 0.0000000000
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 1.0000000000
34 | 1.0000000000
35 |
36 |
37 |
38 |
39 |
40 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/pystache/locator.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides a Locator class for finding template files.
5 |
6 | """
7 |
8 | import os
9 | import re
10 | import sys
11 |
12 | from pystache.common import TemplateNotFoundError
13 | from pystache import defaults
14 |
15 |
16 | class Locator(object):
17 |
18 | def __init__(self, extension=None):
19 | """
20 | Construct a template locator.
21 |
22 | Arguments:
23 |
24 | extension: the template file extension, without the leading dot.
25 | Pass False for no extension (e.g. to use extensionless template
26 | files). Defaults to the package default.
27 |
28 | """
29 | if extension is None:
30 | extension = defaults.TEMPLATE_EXTENSION
31 |
32 | self.template_extension = extension
33 |
34 | def get_object_directory(self, obj):
35 | """
36 | Return the directory containing an object's defining class.
37 |
38 | Returns None if there is no such directory, for example if the
39 | class was defined in an interactive Python session, or in a
40 | doctest that appears in a text file (rather than a Python file).
41 |
42 | """
43 | if not hasattr(obj, '__module__'):
44 | return None
45 |
46 | module = sys.modules[obj.__module__]
47 |
48 | if not hasattr(module, '__file__'):
49 | # TODO: add a unit test for this case.
50 | return None
51 |
52 | path = module.__file__
53 |
54 | return os.path.dirname(path)
55 |
56 | def make_template_name(self, obj):
57 | """
58 | Return the canonical template name for an object instance.
59 |
60 | This method converts Python-style class names (PEP 8's recommended
61 | CamelCase, aka CapWords) to lower_case_with_underscords. Here
62 | is an example with code:
63 |
64 | >>> class HelloWorld(object):
65 | ... pass
66 | >>> hi = HelloWorld()
67 | >>>
68 | >>> locator = Locator()
69 | >>> locator.make_template_name(hi)
70 | 'hello_world'
71 |
72 | """
73 | template_name = obj.__class__.__name__
74 |
75 | def repl(match):
76 | return '_' + match.group(0).lower()
77 |
78 | return re.sub('[A-Z]', repl, template_name)[1:]
79 |
80 | def make_file_name(self, template_name, template_extension=None):
81 | """
82 | Generate and return the file name for the given template name.
83 |
84 | Arguments:
85 |
86 | template_extension: defaults to the instance's extension.
87 |
88 | """
89 | file_name = template_name
90 |
91 | if template_extension is None:
92 | template_extension = self.template_extension
93 |
94 | if template_extension is not False:
95 | file_name += os.path.extsep + template_extension
96 |
97 | return file_name
98 |
99 | def _find_path(self, search_dirs, file_name):
100 | """
101 | Search for the given file, and return the path.
102 |
103 | Returns None if the file is not found.
104 |
105 | """
106 | for dir_path in search_dirs:
107 | file_path = os.path.join(dir_path, file_name)
108 | if os.path.exists(file_path):
109 | return file_path
110 |
111 | return None
112 |
113 | def _find_path_required(self, search_dirs, file_name):
114 | """
115 | Return the path to a template with the given file name.
116 |
117 | """
118 | path = self._find_path(search_dirs, file_name)
119 |
120 | if path is None:
121 | raise TemplateNotFoundError('File %s not found in dirs: %s' %
122 | (repr(file_name), repr(search_dirs)))
123 |
124 | return path
125 |
126 | def find_file(self, file_name, search_dirs):
127 | """
128 | Return the path to a template with the given file name.
129 |
130 | Arguments:
131 |
132 | file_name: the file name of the template.
133 |
134 | search_dirs: the list of directories in which to search.
135 |
136 | """
137 | return self._find_path_required(search_dirs, file_name)
138 |
139 | def find_name(self, template_name, search_dirs):
140 | """
141 | Return the path to a template with the given name.
142 |
143 | Arguments:
144 |
145 | template_name: the name of the template.
146 |
147 | search_dirs: the list of directories in which to search.
148 |
149 | """
150 | file_name = self.make_file_name(template_name)
151 |
152 | return self._find_path_required(search_dirs, file_name)
153 |
154 | def find_object(self, obj, search_dirs, file_name=None):
155 | """
156 | Return the path to a template associated with the given object.
157 |
158 | """
159 | if file_name is None:
160 | # TODO: should we define a make_file_name() method?
161 | template_name = self.make_template_name(obj)
162 | file_name = self.make_file_name(template_name)
163 |
164 | dir_path = self.get_object_directory(obj)
165 |
166 | if dir_path is not None:
167 | search_dirs = [dir_path] + search_dirs
168 |
169 | path = self._find_path_required(search_dirs, file_name)
170 |
171 | return path
172 |
--------------------------------------------------------------------------------
/pystache/loader.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides a Loader class for locating and reading templates.
5 |
6 | """
7 |
8 | import os
9 | import sys
10 |
11 | from pystache import common
12 | from pystache import defaults
13 | from pystache.locator import Locator
14 |
15 |
16 | # We make a function so that the current defaults take effect.
17 | # TODO: revisit whether this is necessary.
18 |
19 | def _make_to_unicode():
20 | def to_unicode(s, encoding=None):
21 | """
22 | Raises a TypeError exception if the given string is already unicode.
23 |
24 | """
25 | if encoding is None:
26 | encoding = defaults.STRING_ENCODING
27 | return str(s, encoding, defaults.DECODE_ERRORS)
28 | return to_unicode
29 |
30 |
31 | class Loader(object):
32 |
33 | """
34 | Loads the template associated to a name or user-defined object.
35 |
36 | All load_*() methods return the template as a unicode string.
37 |
38 | """
39 |
40 | def __init__(self, file_encoding=None, extension=None, to_unicode=None,
41 | search_dirs=None):
42 | """
43 | Construct a template loader instance.
44 |
45 | Arguments:
46 |
47 | extension: the template file extension, without the leading dot.
48 | Pass False for no extension (e.g. to use extensionless template
49 | files). Defaults to the package default.
50 |
51 | file_encoding: the name of the encoding to use when converting file
52 | contents to unicode. Defaults to the package default.
53 |
54 | search_dirs: the list of directories in which to search when loading
55 | a template by name or file name. Defaults to the package default.
56 |
57 | to_unicode: the function to use when converting strings of type
58 | str to unicode. The function should have the signature:
59 |
60 | to_unicode(s, encoding=None)
61 |
62 | It should accept a string of type str and an optional encoding
63 | name and return a string of type unicode. Defaults to calling
64 | Python's built-in function unicode() using the package string
65 | encoding and decode errors defaults.
66 |
67 | """
68 | if extension is None:
69 | extension = defaults.TEMPLATE_EXTENSION
70 |
71 | if file_encoding is None:
72 | file_encoding = defaults.FILE_ENCODING
73 |
74 | if search_dirs is None:
75 | search_dirs = defaults.SEARCH_DIRS
76 |
77 | if to_unicode is None:
78 | to_unicode = _make_to_unicode()
79 |
80 | self.extension = extension
81 | self.file_encoding = file_encoding
82 | # TODO: unit test setting this attribute.
83 | self.search_dirs = search_dirs
84 | self.to_unicode = to_unicode
85 |
86 | def _make_locator(self):
87 | return Locator(extension=self.extension)
88 |
89 | def str(self, s, encoding=None):
90 | """
91 | Convert a string to unicode using the given encoding, and return it.
92 |
93 | This function uses the underlying to_unicode attribute.
94 |
95 | Arguments:
96 |
97 | s: a basestring instance to convert to unicode. Unlike Python's
98 | built-in unicode() function, it is okay to pass unicode strings
99 | to this function. (Passing a unicode string to Python's unicode()
100 | with the encoding argument throws the error, "TypeError: decoding
101 | Unicode is not supported.")
102 |
103 | encoding: the encoding to pass to the to_unicode attribute.
104 | Defaults to None.
105 |
106 | """
107 | if isinstance(s, str):
108 | return str(s)
109 |
110 | return self.to_unicode(s, encoding)
111 |
112 | def read(self, path, encoding=None):
113 | """
114 | Read the template at the given path, and return it as a unicode string.
115 |
116 | """
117 | b = common.read(path)
118 |
119 | if encoding is None:
120 | encoding = self.file_encoding
121 |
122 | return self.str(b, encoding)
123 |
124 | def load_file(self, file_name):
125 | """
126 | Find and return the template with the given file name.
127 |
128 | Arguments:
129 |
130 | file_name: the file name of the template.
131 |
132 | """
133 | locator = self._make_locator()
134 |
135 | path = locator.find_file(file_name, self.search_dirs)
136 |
137 | return self.read(path)
138 |
139 | def load_name(self, name):
140 | """
141 | Find and return the template with the given template name.
142 |
143 | Arguments:
144 |
145 | name: the name of the template.
146 |
147 | """
148 | locator = self._make_locator()
149 |
150 | path = locator.find_name(name, self.search_dirs)
151 |
152 | return self.read(path)
153 |
154 | # TODO: unit-test this method.
155 | def load_object(self, obj):
156 | """
157 | Find and return the template associated to the given object.
158 |
159 | Arguments:
160 |
161 | obj: an instance of a user-defined class.
162 |
163 | search_dirs: the list of directories in which to search.
164 |
165 | """
166 | locator = self._make_locator()
167 |
168 | path = locator.find_object(obj, self.search_dirs)
169 |
170 | return self.read(path)
171 |
--------------------------------------------------------------------------------
/templates/descending-text.sif:
--------------------------------------------------------------------------------
1 |
2 |
159 |
--------------------------------------------------------------------------------
/test/keyframe-import-test_v0.61.sif:
--------------------------------------------------------------------------------
1 |
2 |
168 |
--------------------------------------------------------------------------------
/templates/fromleft-text.sif:
--------------------------------------------------------------------------------
1 |
2 |
3 | Text flying from left template for Keyframe import in SynfigStudio
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 0.0000000000
33 | 0.0000000000
34 |
35 |
36 |
37 |
38 |
39 |
40 | 0.0000000000
41 | 0.0000000000
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 1.0000000000
53 | 1.0000000000
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | this text enters view from left
72 |
73 |
74 |
75 | 0.000000
76 | 0.082076
77 | 0.392097
78 | 1.000000
79 |
80 |
81 |
82 | Sans Serif
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 0.2500000000
99 | 0.2500000000
100 |
101 |
102 |
103 |
104 | 0.0000000000
105 | 0.5000000000
106 |
107 |
108 |
109 |
110 |
111 |
112 | -4.0000000000
113 | 0.0000000000
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/test/keyframe-import-test_v1.0.sif:
--------------------------------------------------------------------------------
1 |
2 |
3 | Synfig Test for Keyframe import
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 0.0000000000
31 | 0.0000000000
32 |
33 |
34 |
35 |
36 |
37 |
38 | 0.0000000000
39 | 0.0000000000
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 1.0000000000
51 | 1.0000000000
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 1.000000
71 | 1.000000
72 | 1.000000
73 | 1.000000
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | this animation has been generated automatically
89 | from an Audacity Labels file, with SynfigStudio.
90 | All Open Source animation
91 |
92 |
93 |
94 | 0.000001
95 | 0.000001
96 | 0.000001
97 | 1.000000
98 |
99 |
100 |
101 | Sans Serif
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 0.1333333403
118 | 0.1333333403
119 |
120 |
121 |
122 |
123 | 0.5000000000
124 | 0.5000000000
125 |
126 |
127 |
128 |
129 | 0.9415920973
130 | -1.7408642769
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Import Labels and Timings in Synfig (Open Source 2D Animation Studio)
2 | ## - aka generate Keyframes and Kinetic Typography (artistic subtitles)
3 |
4 | A [Synfig](http://synfig.org) plug-in to create time reference and/or artistic kinetic text effects in Synfig, from [Audacity](http://audacity.sourceforge.net/) track labels (aka subtitles) or excel/openoffice data.
5 |
6 | Synfig Studio is a free and open-source 2D animation software, designed as powerful industrial-strength solution for creating film-quality animation using a vector and bitmap artwork.
7 |
8 | The generated time reference (keyframes) is great for synchronizing animation with your audio.
9 |
10 | The generated kinetic texts objects is a great basis for beautiful typographic animations, and can be customized easily to achieve your prefered effect(s).
11 |
12 | ## Result
13 |
14 | Different effects are readily available, and it is (rather) easy to create new ones. Check out [our gallery](https://github.com/berteh/synfig-import-labels/wiki/Gallery) for many examples, a typical one is descending text:
15 |
16 | 
17 |
18 | A **demo animation** illustrating many more effects at once is online at https://www.youtube.com/watch?v=F4pDivKQf-g
19 |
20 | Using a simple white background and importing a set of 6 text lines with their timings the following is generated automatically: automatic keyframes (top-right), automatic objects (bottom-right) auto-animated (bottom-left), and ready to be fine-tuned in Synfig before exporting for the world to see ;)
21 | 
22 |
23 | ## Use
24 |
25 | Run the script from within synfig:
26 |
27 | 1. open your synfig project
28 | 1. run the plugin at _> Plug-Ins > Import Labels and Timings_
29 |
30 | or from the command-line:
31 |
32 | python synfig-import-labels.py in.sifz (labels.txt (out.sifz))
33 |
34 | To create your own timed labels file you can:
35 |
36 | 1. use [Audacity to label track segments](http://multimedia.journalism.berkeley.edu/tutorials/audacity/adding-labels/)
37 | 1. export your labels via Tracks > Edit Labels > export, in Audacity.
38 |
39 | or simply use **Notepad** and create a table with 3 "columns": _start time (in sec)_ ``tab`` _end time (in sec)_``tab`` _text (single line)_.
40 | You can add, optionally, add a 4th column with the name of ``template`` to use for this text line and the following ones. Save the file as _labels.txt_ in the directory of your sifz Synfig file project. Titles in the first row are optional, just the data will do just fine, but if you use titles, they must be _exactly_ the following:
41 |
42 | start stop text template
43 | 0.500 1.50 first sentence showing for 1 second descending-text
44 | 1.50 1.90 cool !
45 | 2.40 3.2784 your imagination is the limit ! revealing-text
46 |
47 |
48 | You can use any **spreadsheet** software that is able to export to tsv (_tab separated value_ file), such as OpenOffice or Excel, just make sure to save as UTF-8 if you need proper accents supports.
49 |
50 | Not mentioning any ``template`` column will simply use the default template (see [Configuration](#configuration))
51 |
52 | ## How-to / Tutorial
53 |
54 | The use of this plugin to generate keyframes in a complete animation design is described in the [Synfig Audio Synchronisation tutorial](http://wiki.synfig.org/wiki/Doc:Audio_Synchronisation)
55 |
56 | A tutorial will be written on creating kynetic typographies one day... but I (not secretly) hope you beat me to it!
57 |
58 | ## Install
59 |
60 | Decompress [plugin archive](https://github.com/berteh/synfig-import-labels/archive/master.zip ) into your synfig plugins directory.
61 |
62 | In linux: ``home/-user-/.synfig/plugins``,
63 | in Windows: ``C:\Users\USERNAME\Synfig\plugins``,
64 | in OSX: ``/Users/USERNAME/Library/Synfig/plugins``.
65 | If you are using a portable version of synfig, you will find it at ``ROOT/share/synfig/plugins``.
66 |
67 | Another option is to clone [this repository](https://github.com/berteh/synfig-import-labels.git) in the same location.
68 |
69 | Requirements: Python (Synfig is a recommended option ;) - more info on [Synfig Plugins page](http://wiki.synfig.org/wiki/Doc:Plugins#How_to_install_plugins)
70 |
71 | More information on using plugins is available from the official [Scribus documentation](https://synfig.readthedocs.io/en/latest/plugins.html#how-to-install-plugins).
72 |
73 | ## Configuration
74 |
75 | edit `settings.py` for customisation:
76 |
77 | ```python
78 | # configuration for keyframes import
79 | #
80 | LABELS_FILE = "labels.txt" # audacity labels file name, must be located in your synfig project directory
81 | IMPORT_START = True # set to True to import keyframe for start of label
82 | IMPORT_END = False # set to True to import keyframe for end of label
83 | START_SUFFIX = "" # suffix to add to a label-start keyframe, to distinguish it from label-end frame
84 | END_SUFFIX = " - end" # suffix to add to a label-end keyframe, to distinguish it from label-start frame
85 | OVERWRITE_KEYFRAMES_WITH_SAME_NAME = False # set to True to replace keyframe with exact same description
86 | GENERATE_OBJECTS = True # set to True to generate objects (such as text layers) for each label
87 | #
88 | # settings below only matter to object generation. don't bother if GENERATE_OBJECTS is False.
89 | TEMPLATE_NAME = "appearing-text" # the name of template you want to use. must be located in templates/ subdirectory, with .xml extension. default is "popping-text"
90 | SPLIT_WORDS = False # split each word in a separate object
91 | WAYPOINT_TYPE = "halt" # one of: constant, auto, linear, clamped, halt
92 | RANDOM_ORIGIN = 70 # set to a percentage [0-100] to randomize the object origin in the whole document viewbox (0 will stack them all at [0,0])
93 | ANIMATION_INTERVAL = 0.5 # interval (before and after the label time) used for (in & out) transition, in seconds. default is 0.5
94 | #
95 | ```
96 |
97 | ## Contribution
98 |
99 | Got other ideas or templates? add them to the [wiki](/wiki/Gallery) or [create a pull request](https://help.github.com/articles/using-pull-requests/) to share them!
100 |
101 |
102 | ## Support
103 | Preferably use github's [issues tracker](https://github.com/berteh/synfig-import-labels/issues) for bug reports, feature requests and contributing to this code.
104 |
105 | ## Changelog
106 |
107 | - v0.1 - August 2014 - keyframe generation
108 | - v0.2 - September 2015 - objects generation
109 | - v0.3 - June 2016 - tiny improvements
110 |
111 | ## Licence
112 | GNU-GPL v2, same as [Synfig Studio](http://synfig.org).
113 |
114 | This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
115 |
116 | This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
117 |
118 | You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA or visit http://www.gnu.org/licenses/gpl.html
119 |
--------------------------------------------------------------------------------
/pystache/renderengine.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Defines a class responsible for rendering logic.
5 |
6 | """
7 |
8 | import re
9 |
10 | from pystache.common import is_string
11 | from pystache.parser import parse
12 | import collections
13 |
14 |
15 | def context_get(stack, name):
16 | """
17 | Find and return a name from a ContextStack instance.
18 |
19 | """
20 | return stack.get(name)
21 |
22 |
23 | class RenderEngine(object):
24 |
25 | """
26 | Provides a render() method.
27 |
28 | This class is meant only for internal use.
29 |
30 | As a rule, the code in this class operates on unicode strings where
31 | possible rather than, say, strings of type str or markupsafe.Markup.
32 | This means that strings obtained from "external" sources like partials
33 | and variable tag values are immediately converted to unicode (or
34 | escaped and converted to unicode) before being operated on further.
35 | This makes maintaining, reasoning about, and testing the correctness
36 | of the code much simpler. In particular, it keeps the implementation
37 | of this class independent of the API details of one (or possibly more)
38 | unicode subclasses (e.g. markupsafe.Markup).
39 |
40 | """
41 |
42 | # TODO: it would probably be better for the constructor to accept
43 | # and set as an attribute a single RenderResolver instance
44 | # that encapsulates the customizable aspects of converting
45 | # strings and resolving partials and names from context.
46 | def __init__(self, literal=None, escape=None, resolve_context=None,
47 | resolve_partial=None, to_str=None):
48 | """
49 | Arguments:
50 |
51 | literal: the function used to convert unescaped variable tag
52 | values to unicode, e.g. the value corresponding to a tag
53 | "{{{name}}}". The function should accept a string of type
54 | str or unicode (or a subclass) and return a string of type
55 | unicode (but not a proper subclass of unicode).
56 | This class will only pass basestring instances to this
57 | function. For example, it will call str() on integer variable
58 | values prior to passing them to this function.
59 |
60 | escape: the function used to escape and convert variable tag
61 | values to unicode, e.g. the value corresponding to a tag
62 | "{{name}}". The function should obey the same properties
63 | described above for the "literal" function argument.
64 | This function should take care to convert any str
65 | arguments to unicode just as the literal function should, as
66 | this class will not pass tag values to literal prior to passing
67 | them to this function. This allows for more flexibility,
68 | for example using a custom escape function that handles
69 | incoming strings of type markupsafe.Markup differently
70 | from plain unicode strings.
71 |
72 | resolve_context: the function to call to resolve a name against
73 | a context stack. The function should accept two positional
74 | arguments: a ContextStack instance and a name to resolve.
75 |
76 | resolve_partial: the function to call when loading a partial.
77 | The function should accept a template name string and return a
78 | template string of type unicode (not a subclass).
79 |
80 | to_str: a function that accepts an object and returns a string (e.g.
81 | the built-in function str). This function is used for string
82 | coercion whenever a string is required (e.g. for converting None
83 | or 0 to a string).
84 |
85 | """
86 | self.escape = escape
87 | self.literal = literal
88 | self.resolve_context = resolve_context
89 | self.resolve_partial = resolve_partial
90 | self.to_str = to_str
91 |
92 | # TODO: Rename context to stack throughout this module.
93 |
94 | # From the spec:
95 | #
96 | # When used as the data value for an Interpolation tag, the lambda
97 | # MUST be treatable as an arity 0 function, and invoked as such.
98 | # The returned value MUST be rendered against the default delimiters,
99 | # then interpolated in place of the lambda.
100 | #
101 | def fetch_string(self, context, name):
102 | """
103 | Get a value from the given context as a basestring instance.
104 |
105 | """
106 | val = self.resolve_context(context, name)
107 |
108 | if isinstance(val, collections.Callable):
109 | # Return because _render_value() is already a string.
110 | return self._render_value(val(), context)
111 |
112 | if not is_string(val):
113 | return self.to_str(val)
114 |
115 | return val
116 |
117 | def fetch_section_data(self, context, name):
118 | """
119 | Fetch the value of a section as a list.
120 |
121 | """
122 | data = self.resolve_context(context, name)
123 |
124 | # From the spec:
125 | #
126 | # If the data is not of a list type, it is coerced into a list
127 | # as follows: if the data is truthy (e.g. `!!data == true`),
128 | # use a single-element list containing the data, otherwise use
129 | # an empty list.
130 | #
131 | if not data:
132 | data = []
133 | else:
134 | # The least brittle way to determine whether something
135 | # supports iteration is by trying to call iter() on it:
136 | #
137 | # http://docs.python.org/library/functions.html#iter
138 | #
139 | # It is not sufficient, for example, to check whether the item
140 | # implements __iter__ () (the iteration protocol). There is
141 | # also __getitem__() (the sequence protocol). In Python 2,
142 | # strings do not implement __iter__(), but in Python 3 they do.
143 | try:
144 | iter(data)
145 | except TypeError:
146 | # Then the value does not support iteration.
147 | data = [data]
148 | else:
149 | if is_string(data) or isinstance(data, dict):
150 | # Do not treat strings and dicts (which are iterable) as lists.
151 | data = [data]
152 | # Otherwise, treat the value as a list.
153 |
154 | return data
155 |
156 | def _render_value(self, val, context, delimiters=None):
157 | """
158 | Render an arbitrary value.
159 |
160 | """
161 | if not is_string(val):
162 | # In case the template is an integer, for example.
163 | val = self.to_str(val)
164 | if type(val) is not str:
165 | val = self.literal(val)
166 | return self.render(val, context, delimiters)
167 |
168 | def render(self, template, context_stack, delimiters=None):
169 | """
170 | Render a unicode template string, and return as unicode.
171 |
172 | Arguments:
173 |
174 | template: a template string of type unicode (but not a proper
175 | subclass of unicode).
176 |
177 | context_stack: a ContextStack instance.
178 |
179 | """
180 | parsed_template = parse(template, delimiters)
181 |
182 | return parsed_template.render(self, context_stack)
183 |
--------------------------------------------------------------------------------
/templates/revealing-text.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{origin_x}}
14 | {{origin_y}}
15 |
16 |
17 |
18 |
19 |
20 |
21 | 0.0000000000
22 | 0.0000000000
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 1.0000000000
34 | 1.0000000000
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{text}}
53 |
54 |
55 |
56 | 0.074763
57 | 0.000000
58 | 0.094494
59 | 1.000000
60 |
61 |
62 |
63 | Sans Serif
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 0.2500000000
80 | 0.2500000000
81 |
82 |
83 |
84 |
85 | 0.5000000000
86 | 0.5000000000
87 |
88 |
89 |
90 |
91 | 0.0000000000
92 | 0.0000000000
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 0.0000000000
118 | 0.0000000000
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | 4.0000000000
128 | 0.0000000000
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | 1.0000000000
152 | 1.0000000000
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | 0.0666666701
172 | 0.0000000000
173 |
174 |
175 |
176 |
177 | -0.0666666701
178 | 0.0000000000
179 |
180 |
181 |
182 |
183 |
184 | 1.000000
185 | 1.000000
186 | 1.000000
187 | 1.000000
188 |
189 |
190 | 0.041083
191 | 0.000000
192 | 0.000000
193 | 0.000000
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
--------------------------------------------------------------------------------
/templates/revealing-text.sif:
--------------------------------------------------------------------------------
1 |
2 |
3 | Revealing Text template for Keyframe import in SynfigStudio
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | before&start
19 | end&after
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 0.0000000000
33 | 0.0000000000
34 |
35 |
36 |
37 |
38 |
39 |
40 | 0.0000000000
41 | 0.0000000000
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 1.0000000000
53 | 1.0000000000
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | this text reveals progressively
72 |
73 |
74 |
75 | 0.074763
76 | 0.000000
77 | 0.094494
78 | 1.000000
79 |
80 |
81 |
82 | Sans Serif
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 0.2500000000
99 | 0.2500000000
100 |
101 |
102 |
103 |
104 | 0.5000000000
105 | 0.5000000000
106 |
107 |
108 |
109 |
110 | 0.0000000000
111 | 0.0000000000
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | 0.0000000000
137 | 0.0000000000
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | 4.0000000000
147 | 0.0000000000
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | 1.0000000000
171 | 1.0000000000
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | 0.0666666701
191 | 0.0000000000
192 |
193 |
194 |
195 |
196 | -0.0666666701
197 | 0.0000000000
198 |
199 |
200 |
201 |
202 |
203 | 1.000000
204 | 1.000000
205 | 1.000000
206 | 1.000000
207 |
208 |
209 | 0.041083
210 | 0.000000
211 | 0.000000
212 | 0.000000
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
--------------------------------------------------------------------------------
/synfig-import-labels.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright (c) 2014-2016 by Berteh
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation; either version 2 of the License, or
8 | # (at your option) any later version.
9 | #------------------------------------------------
10 | #
11 | # usage
12 | #
13 | # from synfigstudio (open the project you want to import to)
14 | # caret/right-click > Plug-Ins > Import Labels and Timings
15 | #
16 | # from command line
17 | # python synfig-import-labels.py in.sifz (labels.txt (out.sifz))
18 | #
19 | #
20 | #------------------------------------------------
21 | # installation: see http://wiki.synfig.org/wiki/Doc:Plugins
22 | #
23 | #------------------------------------------------
24 | # Feel free to improve the code below and share your modifications at
25 | # https://github.com/berteh/synfig-import-labels/issues
26 |
27 | import os
28 | import sys
29 | import xml.etree.ElementTree as ET
30 | from xml.sax.saxutils import quoteattr
31 | import re
32 | import settings as s
33 | import random
34 | import csv
35 | import gzip
36 | #import pystache # imported below if s.GENERATE_OBJECTS is True.
37 |
38 | # fix for Synfig seemingly not addding plugin dir to import path
39 | script_dir = os.path.dirname(os.path.realpath(__file__))
40 | if script_dir not in sys.path:
41 | sys.path.insert(0, script_dir)
42 |
43 |
44 | def frames2sec(fps, secFrame): # convert Synfig "Wh Xm Ys Zf" time notation to in seconds.
45 | pattern = "^(?:(\d+)h)?\s?(?:(\d+)m)?\s?(?:(\d+)s)?\s?(?:(\d+)f)?$"
46 | match = re.match(pattern, secFrame)
47 | [h, m, s, f] = match.groups(0)
48 | h2s = float(h)*3600 if h is not None else 0
49 | m2s = float(m)*60 if m is not None else 0
50 | s2s = float(s) if s is not None else 0
51 | f2s = float(f)/fps if f is not None else 0
52 | return h2s + m2s + s2s + f2s
53 |
54 | def sec2Frames(fps, seconds): # convert seconds to [seconds, seconds, frames]
55 | ss = int(seconds)
56 | sf = int(fps * (seconds - ss))
57 | return [seconds, ss, sf]
58 |
59 | def getCsvData(csv_filepath):
60 | # Read CSV file and return 2-dimensional list containing the data
61 | header = s.HEADER
62 | data = []
63 | reader = csv.reader(open(csv_filepath), delimiter=s.TSV_SEPARATOR)
64 | for row in reader:
65 | rowlist = []
66 | for col in row:
67 | rowlist.append(col)
68 | if(rowlist[0]==s.HEADER[0]):
69 | header = rowlist
70 | else:
71 | data.append(rowlist)
72 | return [header, data]
73 |
74 | def process(sifin_filename, labels_filepath, sifout_filename):
75 | #read audacity labels file
76 | labels_filepath = os.path.join(script_dir,labels_filepath)
77 | if not os.path.exists(labels_filepath):
78 | sys.exit("Labels file not found: %s " % labels_filepath)
79 | with open(labels_filepath, 'r') as f:
80 | print(" reading labels from %s" % labels_filepath) # commented for "print" statements lead to syntax error in synfig.
81 | [header,data] = getCsvData(labels_filepath)
82 | if(len(data) < 1):
83 | sys.exit("Data file %s contains no data or could not be parsed, nothing to generate. Halting."%(labels_filepath))
84 |
85 | # read input sif(z) file as xml
86 | if(sifin_filename[-4:] == "sifz"):
87 | sifin = gzip.GzipFile(sifin_filename)
88 | else:
89 | sifin = sifin_filename
90 | tree = ET.parse(sifin)
91 | canvas = tree.getroot()
92 | fps = int(float(canvas.get("fps")))
93 |
94 | # put existing keyframes description in attributes, to enable fast xpath search with no other external module
95 | for key in canvas.findall("./keyframe"):
96 | if key.text is not None :
97 | key.set("desc",quoteattr(key.text))
98 | # print " copied descriptions of keyframes to attributes in sif file:"
99 | # ET.dump(canvas)
100 |
101 | #process labels into keyframes & objects
102 | template = s.TEMPLATE_NAME
103 | texts = []
104 | starts = []
105 | ends = []
106 | templates = []
107 |
108 | for line in data:
109 | #convert audacity time to synfig seconds+frame.
110 | [start,ss,sf] = sec2Frames(fps, float(line[0].replace(",",".")))
111 | [end, es, ef] = sec2Frames(fps, float(line[1].replace(",",".")))
112 | desc = str(line[2])
113 | if ((len(line)>3) and (line[3] != "")):
114 | template = line[3]
115 |
116 | #what follows is reusable for any format, TODO turn into function
117 | if s.IMPORT_START:
118 | k = canvas.find("keyframe[@desc=%s]" % (quoteattr(desc+s.START_SUFFIX)))
119 | if s.OVERWRITE_KEYFRAMES_WITH_SAME_NAME or (k is None):
120 | if (k is None):
121 | #if s.DEBUG: print " creating keyframe: %s" % desc
122 | k = ET.Element('keyframe',{"active": "true"})
123 | k.text = desc+s.START_SUFFIX
124 | canvas.append(k)
125 |
126 | k.set("time", "%ds %df" % (ss, sf)) #length is set automatically by synfig
127 | #elif s.DEBUG: print " skipping existing start keyframe: %s" % desc
128 |
129 | if s.IMPORT_END and not (start == end):
130 | k = canvas.find("keyframe[@desc=%s]" % (quoteattr(desc+s.END_SUFFIX)))
131 | if s.OVERWRITE_KEYFRAMES_WITH_SAME_NAME or (k is None):
132 | if (k is None):
133 | #if s.DEBUG: print " creating keyframe: %s" % desc
134 | k = ET.Element('keyframe',{"active": "true"})
135 | k.text = desc+s.END_SUFFIX
136 | canvas.append(k)
137 |
138 | k.set("time", "%ds %df" % (es, ef))
139 | #elif s.DEBUG: print " skipping existing end keyframe: %s" % desc
140 |
141 | if s.GENERATE_OBJECTS:
142 | texts.append(desc)
143 | starts.append(start)
144 | ends.append(end)
145 | templates.append(template)
146 |
147 | # generate objects
148 | if s.GENERATE_OBJECTS:
149 | print("generating objects")
150 | try:
151 | import pystache
152 | renderer = pystache.Renderer(search_dirs=[os.path.join(script_dir,"templates"),s.TEMPLATE_DIR], file_extension="xml") #todo preferably parse template only once before loop.
153 | except ImportError:
154 | print("Could not load template engine for objects generation.\nPlease verify you have both a pystache and templates subdirectories in %s, or re-download & re-install this plugin."%script_dir)
155 | sys.exit() # skip objects generation
156 |
157 | b = frames2sec(fps, canvas.get("begin-time")) # lower bound for time
158 | e = frames2sec(fps, canvas.get("end-time")) # upper bound for time
159 | d = s.ANIMATION_INTERVAL
160 | r = s.RANDOM_ORIGIN
161 | z = 999934569.1341 # basis for groupid generation, any float > 0 would do.
162 | view = canvas.get("view-box").split()
163 | [minx, maxy, maxx, miny] = [float(elem) for elem in view]
164 |
165 | for i,t in enumerate(texts):
166 |
167 | if s.SPLIT_WORDS:
168 | objects = t.split() # opt add separator as argument, space by default.
169 | objC = len(objects)
170 | objTimes = [starts[i] + (ends[i]-starts[i]) * j for j,word in enumerate(objects)]
171 | else:
172 | objects = [t]
173 | objTimes = [starts[i]]
174 |
175 | #generate object, turn into function
176 | for j,o in enumerate(objects):
177 | if (r>0):
178 | ox = random.uniform(minx, maxx)*r/100
179 | oy = random.uniform(miny, maxy)*r/100
180 | else:
181 | ox = 0
182 | oy = 0
183 |
184 | t1 = max(b,starts[i]-d)
185 | t2 = objTimes[j]
186 | t3 = ends[i]
187 | t4 = min(ends[i]+d,e)
188 |
189 | values = {
190 | 'text':o,
191 | 'value_before':s.VALUE_BEFORE,
192 | 'value_after':s.VALUE_AFTER,
193 | 'loop_before':s.LOOP_BEFORE,
194 | 'loop_middle':s.LOOP_MIDDLE,
195 | 'loop_after':s.LOOP_AFTER,
196 | 'time1':str(t1)+'s',
197 | 'time2':str(t2)+'s',
198 | 'time3':str(t3)+'s',
199 | 'time4':str(t4)+'s',
200 | 'transition':s.WAYPOINT_TYPE,
201 | 'origin_x':ox,
202 | 'origin_y':oy
203 | }
204 | values.update({
205 | 'group1':(z+t1).hex()[4:-4],
206 | 'group2':(z+t2).hex()[4:-4],
207 | 'group3':(z+t3).hex()[4:-4],
208 | 'group4':(z+t4).hex()[4:-4]
209 | })
210 |
211 | #print "1 object is being added to canvas for '%s'"%t
212 | #TODO: give feedback to user in GUI if template not found on TemplateNotFoundError
213 | try:
214 | l = renderer.render_name(templates[i], values)
215 | except pystache.common.TemplateNotFoundError:
216 | sys.exit("Could not load template %s for objects generation. Please verify your 'TEMPLATE_DIR' in settings.py."%templates[i])
217 |
218 | layer = ET.fromstring(l)
219 | canvas.append(layer)
220 | print("added object '%s'"%o)
221 |
222 | # write modified xml tree
223 | if(sifout_filename[-4:] == "sifz"):
224 | sifout_filename = gzip.GzipFile(sifout_filename,'wb')
225 | tree.write(sifout_filename, encoding="UTF-8", xml_declaration=True)
226 |
227 | #main
228 | if len(sys.argv) < 2:
229 | sys.exit("Missing synfig filename as argument")
230 | else:
231 | # defaults
232 | sifin_filename = sys.argv[1]
233 | labels_filepath = sys.argv[2] if len(sys.argv)>2 else os.path.join(os.path.dirname(sifin_filename), s.LABELS_FILE)
234 | sifout_filename = sys.argv[3] if len(sys.argv)>3 else sifin_filename
235 | process(sifin_filename, labels_filepath, sifout_filename)
--------------------------------------------------------------------------------
/pystache/README.md:
--------------------------------------------------------------------------------
1 | Pystache
2 | ========
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 
14 |
15 | 
16 |
17 | [Pystache](http://defunkt.github.com/pystache) is a Python
18 | implementation of [Mustache](http://mustache.github.com/). Mustache is a
19 | framework-agnostic, logic-free templating system inspired by
20 | [ctemplate](http://code.google.com/p/google-ctemplate/) and
21 | [et](http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html).
22 | Like ctemplate, Mustache "emphasizes separating logic from presentation:
23 | it is impossible to embed application logic in this template language."
24 |
25 | The [mustache(5)](http://mustache.github.com/mustache.5.html) man page
26 | provides a good introduction to Mustache's syntax. For a more complete
27 | (and more current) description of Mustache's behavior, see the official
28 | [Mustache spec](https://github.com/mustache/spec).
29 |
30 | Pystache is [semantically versioned](http://semver.org) and can be found
31 | on [PyPI](http://pypi.python.org/pypi/pystache). This version of
32 | Pystache passes all tests in [version
33 | 1.1.2](https://github.com/mustache/spec/tree/v1.1.2) of the spec.
34 |
35 |
36 | Requirements
37 | ------------
38 |
39 | Pystache is tested with--
40 |
41 | - Python 2.4 (requires simplejson [version
42 | 2.0.9](http://pypi.python.org/pypi/simplejson/2.0.9) or earlier)
43 | - Python 2.5 (requires
44 | [simplejson](http://pypi.python.org/pypi/simplejson/))
45 | - Python 2.6
46 | - Python 2.7
47 | - Python 3.1
48 | - Python 3.2
49 | - Python 3.3
50 | - [PyPy](http://pypy.org/)
51 |
52 | [Distribute](http://packages.python.org/distribute/) (the setuptools fork)
53 | is recommended over [setuptools](http://pypi.python.org/pypi/setuptools),
54 | and is required in some cases (e.g. for Python 3 support).
55 | If you use [pip](http://www.pip-installer.org/), you probably already satisfy
56 | this requirement.
57 |
58 | JSON support is needed only for the command-line interface and to run
59 | the spec tests. We require simplejson for earlier versions of Python
60 | since Python's [json](http://docs.python.org/library/json.html) module
61 | was added in Python 2.6.
62 |
63 | For Python 2.4 we require an earlier version of simplejson since
64 | simplejson stopped officially supporting Python 2.4 in simplejson
65 | version 2.1.0. Earlier versions of simplejson can be installed manually,
66 | as follows:
67 |
68 | pip install 'simplejson<2.1.0'
69 |
70 | Official support for Python 2.4 will end with Pystache version 0.6.0.
71 |
72 | Install It
73 | ----------
74 |
75 | pip install pystache
76 |
77 | And test it--
78 |
79 | pystache-test
80 |
81 | To install and test from source (e.g. from GitHub), see the Develop
82 | section.
83 |
84 | Use It
85 | ------
86 |
87 | >>> import pystache
88 | >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
89 | Hi Mom!
90 |
91 | You can also create dedicated view classes to hold your view logic.
92 |
93 | Here's your view class (in .../examples/readme.py):
94 |
95 | class SayHello(object):
96 | def to(self):
97 | return "Pizza"
98 |
99 | Instantiating like so:
100 |
101 | >>> from pystache.tests.examples.readme import SayHello
102 | >>> hello = SayHello()
103 |
104 | Then your template, say\_hello.mustache (by default in the same
105 | directory as your class definition):
106 |
107 | Hello, {{to}}!
108 |
109 | Pull it together:
110 |
111 | >>> renderer = pystache.Renderer()
112 | >>> print renderer.render(hello)
113 | Hello, Pizza!
114 |
115 | For greater control over rendering (e.g. to specify a custom template
116 | directory), use the `Renderer` class like above. One can pass attributes
117 | to the Renderer class constructor or set them on a Renderer instance. To
118 | customize template loading on a per-view basis, subclass `TemplateSpec`.
119 | See the docstrings of the
120 | [Renderer](https://github.com/defunkt/pystache/blob/master/pystache/renderer.py)
121 | class and
122 | [TemplateSpec](https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py)
123 | class for more information.
124 |
125 | You can also pre-parse a template:
126 |
127 | >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}")
128 | >>> print parsed
129 | [u'Hey ', _SectionNode(key=u'who', index_begin=12, index_end=18, parsed=[_EscapeNode(key=u'.'), u'!'])]
130 |
131 | And then:
132 |
133 | >>> print renderer.render(parsed, {'who': 'Pops'})
134 | Hey Pops!
135 | >>> print renderer.render(parsed, {'who': 'you'})
136 | Hey you!
137 |
138 | Python 3
139 | --------
140 |
141 | Pystache has supported Python 3 since version 0.5.1. Pystache behaves
142 | slightly differently between Python 2 and 3, as follows:
143 |
144 | - In Python 2, the default html-escape function `cgi.escape()` does
145 | not escape single quotes. In Python 3, the default escape function
146 | `html.escape()` does escape single quotes.
147 | - In both Python 2 and 3, the string and file encodings default to
148 | `sys.getdefaultencoding()`. However, this function can return
149 | different values under Python 2 and 3, even when run from the same
150 | system. Check your own system for the behavior on your system, or do
151 | not rely on the defaults by passing in the encodings explicitly
152 | (e.g. to the `Renderer` class).
153 |
154 | Unicode
155 | -------
156 |
157 | This section describes how Pystache handles unicode, strings, and
158 | encodings.
159 |
160 | Internally, Pystache uses [only unicode
161 | strings](http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs)
162 | (`str` in Python 3 and `unicode` in Python 2). For input, Pystache
163 | accepts both unicode strings and byte strings (`bytes` in Python 3 and
164 | `str` in Python 2). For output, Pystache's template rendering methods
165 | return only unicode.
166 |
167 | Pystache's `Renderer` class supports a number of attributes to control
168 | how Pystache converts byte strings to unicode on input. These include
169 | the `file_encoding`, `string_encoding`, and `decode_errors` attributes.
170 |
171 | The `file_encoding` attribute is the encoding the renderer uses to
172 | convert to unicode any files read from the file system. Similarly,
173 | `string_encoding` is the encoding the renderer uses to convert any other
174 | byte strings encountered during the rendering process into unicode (e.g.
175 | context values that are encoded byte strings).
176 |
177 | The `decode_errors` attribute is what the renderer passes as the
178 | `errors` argument to Python's built-in unicode-decoding function
179 | (`str()` in Python 3 and `unicode()` in Python 2). The valid values for
180 | this argument are `strict`, `ignore`, and `replace`.
181 |
182 | Each of these attributes can be set via the `Renderer` class's
183 | constructor using a keyword argument of the same name. See the Renderer
184 | class's docstrings for further details. In addition, the `file_encoding`
185 | attribute can be controlled on a per-view basis by subclassing the
186 | `TemplateSpec` class. When not specified explicitly, these attributes
187 | default to values set in Pystache's `defaults` module.
188 |
189 | Develop
190 | -------
191 |
192 | To test from a source distribution (without installing)--
193 |
194 | python test_pystache.py
195 |
196 | To test Pystache with multiple versions of Python (with a single
197 | command!), you can use [tox](http://pypi.python.org/pypi/tox):
198 |
199 | pip install 'virtualenv<1.8' # Version 1.8 dropped support for Python 2.4.
200 | pip install 'tox<1.4' # Version 1.4 dropped support for Python 2.4.
201 | tox
202 |
203 | If you do not have all Python versions listed in `tox.ini`--
204 |
205 | tox -e py26,py32 # for example
206 |
207 | The source distribution tests also include doctests and tests from the
208 | Mustache spec. To include tests from the Mustache spec in your test
209 | runs:
210 |
211 | git submodule init
212 | git submodule update
213 |
214 | The test harness parses the spec's (more human-readable) yaml files if
215 | [PyYAML](http://pypi.python.org/pypi/PyYAML) is present. Otherwise, it
216 | parses the json files. To install PyYAML--
217 |
218 | pip install pyyaml
219 |
220 | To run a subset of the tests, you can use
221 | [nose](http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html):
222 |
223 | pip install nose
224 | nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
225 |
226 | ### Using Python 3 with Pystache from source
227 |
228 | Pystache is written in Python 2 and must be converted to Python 3 prior to
229 | using it with Python 3. The installation process (and tox) do this
230 | automatically.
231 |
232 | To convert the code to Python 3 manually (while using Python 3)--
233 |
234 | python setup.py build
235 |
236 | This writes the converted code to a subdirectory called `build`.
237 | By design, Python 3 builds
238 | [cannot](https://bitbucket.org/tarek/distribute/issue/292/allow-use_2to3-with-python-2)
239 | be created from Python 2.
240 |
241 | To convert the code without using setup.py, you can use
242 | [2to3](http://docs.python.org/library/2to3.html) as follows (two steps)--
243 |
244 | 2to3 --write --nobackups --no-diffs --doctests_only pystache
245 | 2to3 --write --nobackups --no-diffs pystache
246 |
247 | This converts the code (and doctests) in place.
248 |
249 | To `import pystache` from a source distribution while using Python 3, be
250 | sure that you are importing from a directory containing a converted
251 | version of the code (e.g. from the `build` directory after converting),
252 | and not from the original (unconverted) source directory. Otherwise, you will
253 | get a syntax error. You can help prevent this by not running the Python
254 | IDE from the project directory when importing Pystache while using Python 3.
255 |
256 |
257 | Mailing List
258 | ------------
259 |
260 | There is a [mailing list](http://librelist.com/browser/pystache/). Note
261 | that there is a bit of a delay between posting a message and seeing it
262 | appear in the mailing list archive.
263 |
264 | Credits
265 | -------
266 |
267 | >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
268 | >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
269 | Author: Chris Wanstrath
270 | Maintainer: Chris Jerdonek
271 |
272 | Pystache logo by [David Phillips](http://davidphillips.us/) is licensed
273 | under a [Creative Commons Attribution-ShareAlike 3.0 Unported
274 | License](http://creativecommons.org/licenses/by-sa/3.0/deed.en_US).
275 | 
277 |
--------------------------------------------------------------------------------
/pystache/context.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Exposes a ContextStack class.
5 |
6 | The Mustache spec makes a special distinction between two types of context
7 | stack elements: hashes and objects. For the purposes of interpreting the
8 | spec, we define these categories mutually exclusively as follows:
9 |
10 | (1) Hash: an item whose type is a subclass of dict.
11 |
12 | (2) Object: an item that is neither a hash nor an instance of a
13 | built-in type.
14 |
15 | """
16 |
17 | from pystache.common import PystacheError
18 | import collections
19 |
20 |
21 | # This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
22 | _BUILTIN_MODULE = type(0).__module__
23 |
24 |
25 | # We use this private global variable as a return value to represent a key
26 | # not being found on lookup. This lets us distinguish between the case
27 | # of a key's value being None with the case of a key not being found --
28 | # without having to rely on exceptions (e.g. KeyError) for flow control.
29 | #
30 | # TODO: eliminate the need for a private global variable, e.g. by using the
31 | # preferred Python approach of "easier to ask for forgiveness than permission":
32 | # http://docs.python.org/glossary.html#term-eafp
33 | class NotFound(object):
34 | pass
35 | _NOT_FOUND = NotFound()
36 |
37 |
38 | def _get_value(context, key):
39 | """
40 | Retrieve a key's value from a context item.
41 |
42 | Returns _NOT_FOUND if the key does not exist.
43 |
44 | The ContextStack.get() docstring documents this function's intended behavior.
45 |
46 | """
47 | if isinstance(context, dict):
48 | # Then we consider the argument a "hash" for the purposes of the spec.
49 | #
50 | # We do a membership test to avoid using exceptions for flow control
51 | # (e.g. catching KeyError).
52 | if key in context:
53 | return context[key]
54 | elif type(context).__module__ != _BUILTIN_MODULE:
55 | # Then we consider the argument an "object" for the purposes of
56 | # the spec.
57 | #
58 | # The elif test above lets us avoid treating instances of built-in
59 | # types like integers and strings as objects (cf. issue #81).
60 | # Instances of user-defined classes on the other hand, for example,
61 | # are considered objects by the test above.
62 | try:
63 | attr = getattr(context, key)
64 | except AttributeError:
65 | # TODO: distinguish the case of the attribute not existing from
66 | # an AttributeError being raised by the call to the attribute.
67 | # See the following issue for implementation ideas:
68 | # http://bugs.python.org/issue7559
69 | pass
70 | else:
71 | # TODO: consider using EAFP here instead.
72 | # http://docs.python.org/glossary.html#term-eafp
73 | if isinstance(attr, collections.Callable):
74 | return attr()
75 | return attr
76 |
77 | return _NOT_FOUND
78 |
79 |
80 | class KeyNotFoundError(PystacheError):
81 |
82 | """
83 | An exception raised when a key is not found in a context stack.
84 |
85 | """
86 |
87 | def __init__(self, key, details):
88 | self.key = key
89 | self.details = details
90 |
91 | def __str__(self):
92 | return "Key %s not found: %s" % (repr(self.key), self.details)
93 |
94 |
95 | class ContextStack(object):
96 |
97 | """
98 | Provides dictionary-like access to a stack of zero or more items.
99 |
100 | Instances of this class are meant to act as the rendering context
101 | when rendering Mustache templates in accordance with mustache(5)
102 | and the Mustache spec.
103 |
104 | Instances encapsulate a private stack of hashes, objects, and built-in
105 | type instances. Querying the stack for the value of a key queries
106 | the items in the stack in order from last-added objects to first
107 | (last in, first out).
108 |
109 | Caution: this class does not currently support recursive nesting in
110 | that items in the stack cannot themselves be ContextStack instances.
111 |
112 | See the docstrings of the methods of this class for more details.
113 |
114 | """
115 |
116 | # We reserve keyword arguments for future options (e.g. a "strict=True"
117 | # option for enabling a strict mode).
118 | def __init__(self, *items):
119 | """
120 | Construct an instance, and initialize the private stack.
121 |
122 | The *items arguments are the items with which to populate the
123 | initial stack. Items in the argument list are added to the
124 | stack in order so that, in particular, items at the end of
125 | the argument list are queried first when querying the stack.
126 |
127 | Caution: items should not themselves be ContextStack instances, as
128 | recursive nesting does not behave as one might expect.
129 |
130 | """
131 | self._stack = list(items)
132 |
133 | def __repr__(self):
134 | """
135 | Return a string representation of the instance.
136 |
137 | For example--
138 |
139 | >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
140 | >>> repr(context)
141 | "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
142 |
143 | """
144 | return "%s%s" % (self.__class__.__name__, tuple(self._stack))
145 |
146 | @staticmethod
147 | def create(*context, **kwargs):
148 | """
149 | Build a ContextStack instance from a sequence of context-like items.
150 |
151 | This factory-style method is more general than the ContextStack class's
152 | constructor in that, unlike the constructor, the argument list
153 | can itself contain ContextStack instances.
154 |
155 | Here is an example illustrating various aspects of this method:
156 |
157 | >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
158 | >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
159 | >>>
160 | >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
161 | >>>
162 | >>> context.get('animal')
163 | 'cat'
164 | >>> context.get('vegetable')
165 | 'spinach'
166 | >>> context.get('mineral')
167 | 'gold'
168 |
169 | Arguments:
170 |
171 | *context: zero or more dictionaries, ContextStack instances, or objects
172 | with which to populate the initial context stack. None
173 | arguments will be skipped. Items in the *context list are
174 | added to the stack in order so that later items in the argument
175 | list take precedence over earlier items. This behavior is the
176 | same as the constructor's.
177 |
178 | **kwargs: additional key-value data to add to the context stack.
179 | As these arguments appear after all items in the *context list,
180 | in the case of key conflicts these values take precedence over
181 | all items in the *context list. This behavior is the same as
182 | the constructor's.
183 |
184 | """
185 | items = context
186 |
187 | context = ContextStack()
188 |
189 | for item in items:
190 | if item is None:
191 | continue
192 | if isinstance(item, ContextStack):
193 | context._stack.extend(item._stack)
194 | else:
195 | context.push(item)
196 |
197 | if kwargs:
198 | context.push(kwargs)
199 |
200 | return context
201 |
202 | # TODO: add more unit tests for this.
203 | # TODO: update the docstring for dotted names.
204 | def get(self, name):
205 | """
206 | Resolve a dotted name against the current context stack.
207 |
208 | This function follows the rules outlined in the section of the
209 | spec regarding tag interpolation. This function returns the value
210 | as is and does not coerce the return value to a string.
211 |
212 | Arguments:
213 |
214 | name: a dotted or non-dotted name.
215 |
216 | default: the value to return if name resolution fails at any point.
217 | Defaults to the empty string per the Mustache spec.
218 |
219 | This method queries items in the stack in order from last-added
220 | objects to first (last in, first out). The value returned is
221 | the value of the key in the first item that contains the key.
222 | If the key is not found in any item in the stack, then the default
223 | value is returned. The default value defaults to None.
224 |
225 | In accordance with the spec, this method queries items in the
226 | stack for a key differently depending on whether the item is a
227 | hash, object, or neither (as defined in the module docstring):
228 |
229 | (1) Hash: if the item is a hash, then the key's value is the
230 | dictionary value of the key. If the dictionary doesn't contain
231 | the key, then the key is considered not found.
232 |
233 | (2) Object: if the item is an an object, then the method looks for
234 | an attribute with the same name as the key. If an attribute
235 | with that name exists, the value of the attribute is returned.
236 | If the attribute is callable, however (i.e. if the attribute
237 | is a method), then the attribute is called with no arguments
238 | and that value is returned. If there is no attribute with
239 | the same name as the key, then the key is considered not found.
240 |
241 | (3) Neither: if the item is neither a hash nor an object, then
242 | the key is considered not found.
243 |
244 | *Caution*:
245 |
246 | Callables are handled differently depending on whether they are
247 | dictionary values, as in (1) above, or attributes, as in (2).
248 | The former are returned as-is, while the latter are first
249 | called and that value returned.
250 |
251 | Here is an example to illustrate:
252 |
253 | >>> def greet():
254 | ... return "Hi Bob!"
255 | >>>
256 | >>> class Greeter(object):
257 | ... greet = None
258 | >>>
259 | >>> dct = {'greet': greet}
260 | >>> obj = Greeter()
261 | >>> obj.greet = greet
262 | >>>
263 | >>> dct['greet'] is obj.greet
264 | True
265 | >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
266 |
267 | >>> ContextStack(obj).get('greet')
268 | 'Hi Bob!'
269 |
270 | TODO: explain the rationale for this difference in treatment.
271 |
272 | """
273 | if name == '.':
274 | try:
275 | return self.top()
276 | except IndexError:
277 | raise KeyNotFoundError(".", "empty context stack")
278 |
279 | parts = name.split('.')
280 |
281 | try:
282 | result = self._get_simple(parts[0])
283 | except KeyNotFoundError:
284 | raise KeyNotFoundError(name, "first part")
285 |
286 | for part in parts[1:]:
287 | # The full context stack is not used to resolve the remaining parts.
288 | # From the spec--
289 | #
290 | # 5) If any name parts were retained in step 1, each should be
291 | # resolved against a context stack containing only the result
292 | # from the former resolution. If any part fails resolution, the
293 | # result should be considered falsey, and should interpolate as
294 | # the empty string.
295 | #
296 | # TODO: make sure we have a test case for the above point.
297 | result = _get_value(result, part)
298 | # TODO: consider using EAFP here instead.
299 | # http://docs.python.org/glossary.html#term-eafp
300 | if result is _NOT_FOUND:
301 | raise KeyNotFoundError(name, "missing %s" % repr(part))
302 |
303 | return result
304 |
305 | def _get_simple(self, name):
306 | """
307 | Query the stack for a non-dotted name.
308 |
309 | """
310 | for item in reversed(self._stack):
311 | result = _get_value(item, name)
312 | if result is not _NOT_FOUND:
313 | return result
314 |
315 | raise KeyNotFoundError(name, "part missing")
316 |
317 | def push(self, item):
318 | """
319 | Push an item onto the stack.
320 |
321 | """
322 | self._stack.append(item)
323 |
324 | def pop(self):
325 | """
326 | Pop an item off of the stack, and return it.
327 |
328 | """
329 | return self._stack.pop()
330 |
331 | def top(self):
332 | """
333 | Return the item last added to the stack.
334 |
335 | """
336 | return self._stack[-1]
337 |
338 | def copy(self):
339 | """
340 | Return a copy of this instance.
341 |
342 | """
343 | return ContextStack(*self._stack)
344 |
--------------------------------------------------------------------------------
/pystache/parser.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | Exposes a parse() function to parse template strings.
5 |
6 | """
7 |
8 | import re
9 |
10 | from pystache import defaults
11 | from pystache.parsed import ParsedTemplate
12 | import collections
13 |
14 |
15 | END_OF_LINE_CHARACTERS = ['\r', '\n']
16 | NON_BLANK_RE = re.compile(r'^(.)', re.M)
17 |
18 |
19 | # TODO: add some unit tests for this.
20 | # TODO: add a test case that checks for spurious spaces.
21 | # TODO: add test cases for delimiters.
22 | def parse(template, delimiters=None):
23 | """
24 | Parse a unicode template string and return a ParsedTemplate instance.
25 |
26 | Arguments:
27 |
28 | template: a unicode template string.
29 |
30 | delimiters: a 2-tuple of delimiters. Defaults to the package default.
31 |
32 | Examples:
33 |
34 | >>> parsed = parse("Hey {{#who}}{{name}}!{{/who}}")
35 | >>> print(str(parsed).replace('u', '')) # This is a hack to get the test to pass both in Python 2 and 3.
36 | ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])]
37 |
38 | """
39 | if type(template) is not str:
40 | raise Exception("Template is not unicode: %s" % type(template))
41 | parser = _Parser(delimiters)
42 | return parser.parse(template)
43 |
44 |
45 | def _compile_template_re(delimiters):
46 | """
47 | Return a regular expression object (re.RegexObject) instance.
48 |
49 | """
50 | # The possible tag type characters following the opening tag,
51 | # excluding "=" and "{".
52 | tag_types = "!>&/#^"
53 |
54 | # TODO: are we following this in the spec?
55 | #
56 | # The tag's content MUST be a non-whitespace character sequence
57 | # NOT containing the current closing delimiter.
58 | #
59 | tag = r"""
60 | (?P[\ \t]*)
61 | %(otag)s \s*
62 | (?:
63 | (?P=) \s* (?P.+?) \s* = |
64 | (?P{) \s* (?P.+?) \s* } |
65 | (?P[%(tag_types)s]?) \s* (?P[\s\S]+?)
66 | )
67 | \s* %(ctag)s
68 | """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}
69 |
70 | return re.compile(tag, re.VERBOSE)
71 |
72 |
73 | class ParsingError(Exception):
74 |
75 | pass
76 |
77 |
78 | ## Node types
79 |
80 | def _format(obj, exclude=None):
81 | if exclude is None:
82 | exclude = []
83 | exclude.append('key')
84 | attrs = obj.__dict__
85 | names = list(set(attrs.keys()) - set(exclude))
86 | names.sort()
87 | names.insert(0, 'key')
88 | args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
89 | return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))
90 |
91 |
92 | class _CommentNode(object):
93 |
94 | def __repr__(self):
95 | return _format(self)
96 |
97 | def render(self, engine, context):
98 | return ''
99 |
100 |
101 | class _ChangeNode(object):
102 |
103 | def __init__(self, delimiters):
104 | self.delimiters = delimiters
105 |
106 | def __repr__(self):
107 | return _format(self)
108 |
109 | def render(self, engine, context):
110 | return ''
111 |
112 |
113 | class _EscapeNode(object):
114 |
115 | def __init__(self, key):
116 | self.key = key
117 |
118 | def __repr__(self):
119 | return _format(self)
120 |
121 | def render(self, engine, context):
122 | s = engine.fetch_string(context, self.key)
123 | return engine.escape(s)
124 |
125 |
126 | class _LiteralNode(object):
127 |
128 | def __init__(self, key):
129 | self.key = key
130 |
131 | def __repr__(self):
132 | return _format(self)
133 |
134 | def render(self, engine, context):
135 | s = engine.fetch_string(context, self.key)
136 | return engine.literal(s)
137 |
138 |
139 | class _PartialNode(object):
140 |
141 | def __init__(self, key, indent):
142 | self.key = key
143 | self.indent = indent
144 |
145 | def __repr__(self):
146 | return _format(self)
147 |
148 | def render(self, engine, context):
149 | template = engine.resolve_partial(self.key)
150 | # Indent before rendering.
151 | template = re.sub(NON_BLANK_RE, self.indent + r'\1', template)
152 |
153 | return engine.render(template, context)
154 |
155 |
156 | class _InvertedNode(object):
157 |
158 | def __init__(self, key, parsed_section):
159 | self.key = key
160 | self.parsed_section = parsed_section
161 |
162 | def __repr__(self):
163 | return _format(self)
164 |
165 | def render(self, engine, context):
166 | # TODO: is there a bug because we are not using the same
167 | # logic as in fetch_string()?
168 | data = engine.resolve_context(context, self.key)
169 | # Note that lambdas are considered truthy for inverted sections
170 | # per the spec.
171 | if data:
172 | return ''
173 | return self.parsed_section.render(engine, context)
174 |
175 |
176 | class _SectionNode(object):
177 |
178 | # TODO: the template_ and parsed_template_ arguments don't both seem
179 | # to be necessary. Can we remove one of them? For example, if
180 | # callable(data) is True, then the initial parsed_template isn't used.
181 | def __init__(self, key, parsed, delimiters, template, index_begin, index_end):
182 | self.delimiters = delimiters
183 | self.key = key
184 | self.parsed = parsed
185 | self.template = template
186 | self.index_begin = index_begin
187 | self.index_end = index_end
188 |
189 | def __repr__(self):
190 | return _format(self, exclude=['delimiters', 'template'])
191 |
192 | def render(self, engine, context):
193 | values = engine.fetch_section_data(context, self.key)
194 |
195 | parts = []
196 | for val in values:
197 | if isinstance(val, collections.Callable):
198 | # Lambdas special case section rendering and bypass pushing
199 | # the data value onto the context stack. From the spec--
200 | #
201 | # When used as the data value for a Section tag, the
202 | # lambda MUST be treatable as an arity 1 function, and
203 | # invoked as such (passing a String containing the
204 | # unprocessed section contents). The returned value
205 | # MUST be rendered against the current delimiters, then
206 | # interpolated in place of the section.
207 | #
208 | # Also see--
209 | #
210 | # https://github.com/defunkt/pystache/issues/113
211 | #
212 | # TODO: should we check the arity?
213 | val = val(self.template[self.index_begin:self.index_end])
214 | val = engine._render_value(val, context, delimiters=self.delimiters)
215 | parts.append(val)
216 | continue
217 |
218 | context.push(val)
219 | parts.append(self.parsed.render(engine, context))
220 | context.pop()
221 |
222 | return str(''.join(parts))
223 |
224 |
225 | class _Parser(object):
226 |
227 | _delimiters = None
228 | _template_re = None
229 |
230 | def __init__(self, delimiters=None):
231 | if delimiters is None:
232 | delimiters = defaults.DELIMITERS
233 |
234 | self._delimiters = delimiters
235 |
236 | def _compile_delimiters(self):
237 | self._template_re = _compile_template_re(self._delimiters)
238 |
239 | def _change_delimiters(self, delimiters):
240 | self._delimiters = delimiters
241 | self._compile_delimiters()
242 |
243 | def parse(self, template):
244 | """
245 | Parse a template string starting at some index.
246 |
247 | This method uses the current tag delimiter.
248 |
249 | Arguments:
250 |
251 | template: a unicode string that is the template to parse.
252 |
253 | index: the index at which to start parsing.
254 |
255 | Returns:
256 |
257 | a ParsedTemplate instance.
258 |
259 | """
260 | self._compile_delimiters()
261 |
262 | start_index = 0
263 | content_end_index, parsed_section, section_key = None, None, None
264 | parsed_template = ParsedTemplate()
265 |
266 | states = []
267 |
268 | while True:
269 | match = self._template_re.search(template, start_index)
270 |
271 | if match is None:
272 | break
273 |
274 | match_index = match.start()
275 | end_index = match.end()
276 |
277 | matches = match.groupdict()
278 |
279 | # Normalize the matches dictionary.
280 | if matches['change'] is not None:
281 | matches.update(tag='=', tag_key=matches['delims'])
282 | elif matches['raw'] is not None:
283 | matches.update(tag='&', tag_key=matches['raw_name'])
284 |
285 | tag_type = matches['tag']
286 | tag_key = matches['tag_key']
287 | leading_whitespace = matches['whitespace']
288 |
289 | # Standalone (non-interpolation) tags consume the entire line,
290 | # both leading whitespace and trailing newline.
291 | did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
292 | did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
293 | is_tag_interpolating = tag_type in ['', '&']
294 |
295 | if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
296 | if end_index < len(template):
297 | end_index += template[end_index] == '\r' and 1 or 0
298 | if end_index < len(template):
299 | end_index += template[end_index] == '\n' and 1 or 0
300 | elif leading_whitespace:
301 | match_index += len(leading_whitespace)
302 | leading_whitespace = ''
303 |
304 | # Avoid adding spurious empty strings to the parse tree.
305 | if start_index != match_index:
306 | parsed_template.add(template[start_index:match_index])
307 |
308 | start_index = end_index
309 |
310 | if tag_type in ('#', '^'):
311 | # Cache current state.
312 | state = (tag_type, end_index, section_key, parsed_template)
313 | states.append(state)
314 |
315 | # Initialize new state
316 | section_key, parsed_template = tag_key, ParsedTemplate()
317 | continue
318 |
319 | if tag_type == '/':
320 | if tag_key != section_key:
321 | raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
322 |
323 | # Restore previous state with newly found section data.
324 | parsed_section = parsed_template
325 |
326 | (tag_type, section_start_index, section_key, parsed_template) = states.pop()
327 | node = self._make_section_node(template, tag_type, tag_key, parsed_section,
328 | section_start_index, match_index)
329 |
330 | else:
331 | node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
332 |
333 | parsed_template.add(node)
334 |
335 | # Avoid adding spurious empty strings to the parse tree.
336 | if start_index != len(template):
337 | parsed_template.add(template[start_index:])
338 |
339 | return parsed_template
340 |
341 | def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
342 | """
343 | Create and return a non-section node for the parse tree.
344 |
345 | """
346 | # TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
347 | if tag_type == '!':
348 | return _CommentNode()
349 |
350 | if tag_type == '=':
351 | delimiters = tag_key.split()
352 | self._change_delimiters(delimiters)
353 | return _ChangeNode(delimiters)
354 |
355 | if tag_type == '':
356 | return _EscapeNode(tag_key)
357 |
358 | if tag_type == '&':
359 | return _LiteralNode(tag_key)
360 |
361 | if tag_type == '>':
362 | return _PartialNode(tag_key, leading_whitespace)
363 |
364 | raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
365 |
366 | def _make_section_node(self, template, tag_type, tag_key, parsed_section,
367 | section_start_index, section_end_index):
368 | """
369 | Create and return a section node for the parse tree.
370 |
371 | """
372 | if tag_type == '#':
373 | return _SectionNode(tag_key, parsed_section, self._delimiters,
374 | template, section_start_index, section_end_index)
375 |
376 | if tag_type == '^':
377 | return _InvertedNode(tag_key, parsed_section)
378 |
379 | raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
380 |
381 |
--------------------------------------------------------------------------------
/pystache/renderer.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | """
4 | This module provides a Renderer class to render templates.
5 |
6 | """
7 |
8 | import sys
9 |
10 | from pystache import defaults
11 | from pystache.common import TemplateNotFoundError, MissingTags, is_string
12 | from pystache.context import ContextStack, KeyNotFoundError
13 | from pystache.loader import Loader
14 | from pystache.parsed import ParsedTemplate
15 | from pystache.renderengine import context_get, RenderEngine
16 | from pystache.specloader import SpecLoader
17 | from pystache.template_spec import TemplateSpec
18 |
19 |
20 | class Renderer(object):
21 |
22 | """
23 | A class for rendering mustache templates.
24 |
25 | This class supports several rendering options which are described in
26 | the constructor's docstring. Other behavior can be customized by
27 | subclassing this class.
28 |
29 | For example, one can pass a string-string dictionary to the constructor
30 | to bypass loading partials from the file system:
31 |
32 | >>> partials = {'partial': 'Hello, {{thing}}!'}
33 | >>> renderer = Renderer(partials=partials)
34 | >>> # We apply print to make the test work in Python 3 after 2to3.
35 | >>> print(renderer.render('{{>partial}}', {'thing': 'world'}))
36 | Hello, world!
37 |
38 | To customize string coercion (e.g. to render False values as ''), one can
39 | subclass this class. For example:
40 |
41 | class MyRenderer(Renderer):
42 | def str_coerce(self, val):
43 | if not val:
44 | return ''
45 | else:
46 | return str(val)
47 |
48 | """
49 |
50 | def __init__(self, file_encoding=None, string_encoding=None,
51 | decode_errors=None, search_dirs=None, file_extension=None,
52 | escape=None, partials=None, missing_tags=None):
53 | """
54 | Construct an instance.
55 |
56 | Arguments:
57 |
58 | file_encoding: the name of the encoding to use by default when
59 | reading template files. All templates are converted to unicode
60 | prior to parsing. Defaults to the package default.
61 |
62 | string_encoding: the name of the encoding to use when converting
63 | to unicode any byte strings (type str in Python 2) encountered
64 | during the rendering process. This name will be passed as the
65 | encoding argument to the built-in function unicode().
66 | Defaults to the package default.
67 |
68 | decode_errors: the string to pass as the errors argument to the
69 | built-in function unicode() when converting byte strings to
70 | unicode. Defaults to the package default.
71 |
72 | search_dirs: the list of directories in which to search when
73 | loading a template by name or file name. If given a string,
74 | the method interprets the string as a single directory.
75 | Defaults to the package default.
76 |
77 | file_extension: the template file extension. Pass False for no
78 | extension (i.e. to use extensionless template files).
79 | Defaults to the package default.
80 |
81 | partials: an object (e.g. a dictionary) for custom partial loading
82 | during the rendering process.
83 | The object should have a get() method that accepts a string
84 | and returns the corresponding template as a string, preferably
85 | as a unicode string. If there is no template with that name,
86 | the get() method should either return None (as dict.get() does)
87 | or raise an exception.
88 | If this argument is None, the rendering process will use
89 | the normal procedure of locating and reading templates from
90 | the file system -- using relevant instance attributes like
91 | search_dirs, file_encoding, etc.
92 |
93 | escape: the function used to escape variable tag values when
94 | rendering a template. The function should accept a unicode
95 | string (or subclass of unicode) and return an escaped string
96 | that is again unicode (or a subclass of unicode).
97 | This function need not handle strings of type `str` because
98 | this class will only pass it unicode strings. The constructor
99 | assigns this function to the constructed instance's escape()
100 | method.
101 | To disable escaping entirely, one can pass `lambda u: u`
102 | as the escape function, for example. One may also wish to
103 | consider using markupsafe's escape function: markupsafe.escape().
104 | This argument defaults to the package default.
105 |
106 | missing_tags: a string specifying how to handle missing tags.
107 | If 'strict', an error is raised on a missing tag. If 'ignore',
108 | the value of the tag is the empty string. Defaults to the
109 | package default.
110 |
111 | """
112 | if decode_errors is None:
113 | decode_errors = defaults.DECODE_ERRORS
114 |
115 | if escape is None:
116 | escape = defaults.TAG_ESCAPE
117 |
118 | if file_encoding is None:
119 | file_encoding = defaults.FILE_ENCODING
120 |
121 | if file_extension is None:
122 | file_extension = defaults.TEMPLATE_EXTENSION
123 |
124 | if missing_tags is None:
125 | missing_tags = defaults.MISSING_TAGS
126 |
127 | if search_dirs is None:
128 | search_dirs = defaults.SEARCH_DIRS
129 |
130 | if string_encoding is None:
131 | string_encoding = defaults.STRING_ENCODING
132 |
133 | if isinstance(search_dirs, str):
134 | search_dirs = [search_dirs]
135 |
136 | self._context = None
137 | self.decode_errors = decode_errors
138 | self.escape = escape
139 | self.file_encoding = file_encoding
140 | self.file_extension = file_extension
141 | self.missing_tags = missing_tags
142 | self.partials = partials
143 | self.search_dirs = search_dirs
144 | self.string_encoding = string_encoding
145 |
146 | # This is an experimental way of giving views access to the current context.
147 | # TODO: consider another approach of not giving access via a property,
148 | # but instead letting the caller pass the initial context to the
149 | # main render() method by reference. This approach would probably
150 | # be less likely to be misused.
151 | @property
152 | def context(self):
153 | """
154 | Return the current rendering context [experimental].
155 |
156 | """
157 | return self._context
158 |
159 | # We could not choose str() as the name because 2to3 renames the unicode()
160 | # method of this class to str().
161 | def str_coerce(self, val):
162 | """
163 | Coerce a non-string value to a string.
164 |
165 | This method is called whenever a non-string is encountered during the
166 | rendering process when a string is needed (e.g. if a context value
167 | for string interpolation is not a string). To customize string
168 | coercion, you can override this method.
169 |
170 | """
171 | return str(val)
172 |
173 | def _to_unicode_soft(self, s):
174 | """
175 | Convert a basestring to unicode, preserving any unicode subclass.
176 |
177 | """
178 | # We type-check to avoid "TypeError: decoding Unicode is not supported".
179 | # We avoid the Python ternary operator for Python 2.4 support.
180 | if isinstance(s, str):
181 | return s
182 | return self.str(s)
183 |
184 | def _to_unicode_hard(self, s):
185 | """
186 | Convert a basestring to a string with type unicode (not subclass).
187 |
188 | """
189 | return str(self._to_unicode_soft(s))
190 |
191 | def _escape_to_unicode(self, s):
192 | """
193 | Convert a basestring to unicode (preserving any unicode subclass), and escape it.
194 |
195 | Returns a unicode string (not subclass).
196 |
197 | """
198 | return str(self.escape(self._to_unicode_soft(s)))
199 |
200 | def str(self, b, encoding=None):
201 | """
202 | Convert a byte string to unicode, using string_encoding and decode_errors.
203 |
204 | Arguments:
205 |
206 | b: a byte string.
207 |
208 | encoding: the name of an encoding. Defaults to the string_encoding
209 | attribute for this instance.
210 |
211 | Raises:
212 |
213 | TypeError: Because this method calls Python's built-in unicode()
214 | function, this method raises the following exception if the
215 | given string is already unicode:
216 |
217 | TypeError: decoding Unicode is not supported
218 |
219 | """
220 | if encoding is None:
221 | encoding = self.string_encoding
222 |
223 | # TODO: Wrap UnicodeDecodeErrors with a message about setting
224 | # the string_encoding and decode_errors attributes.
225 | return str(b, encoding, self.decode_errors)
226 |
227 | def _make_loader(self):
228 | """
229 | Create a Loader instance using current attributes.
230 |
231 | """
232 | return Loader(file_encoding=self.file_encoding, extension=self.file_extension,
233 | to_unicode=self.str, search_dirs=self.search_dirs)
234 |
235 | def _make_load_template(self):
236 | """
237 | Return a function that loads a template by name.
238 |
239 | """
240 | loader = self._make_loader()
241 |
242 | def load_template(template_name):
243 | return loader.load_name(template_name)
244 |
245 | return load_template
246 |
247 | def _make_load_partial(self):
248 | """
249 | Return a function that loads a partial by name.
250 |
251 | """
252 | if self.partials is None:
253 | return self._make_load_template()
254 |
255 | # Otherwise, create a function from the custom partial loader.
256 | partials = self.partials
257 |
258 | def load_partial(name):
259 | # TODO: consider using EAFP here instead.
260 | # http://docs.python.org/glossary.html#term-eafp
261 | # This would mean requiring that the custom partial loader
262 | # raise a KeyError on name not found.
263 | template = partials.get(name)
264 | if template is None:
265 | raise TemplateNotFoundError("Name %s not found in partials: %s" %
266 | (repr(name), type(partials)))
267 |
268 | # RenderEngine requires that the return value be unicode.
269 | return self._to_unicode_hard(template)
270 |
271 | return load_partial
272 |
273 | def _is_missing_tags_strict(self):
274 | """
275 | Return whether missing_tags is set to strict.
276 |
277 | """
278 | val = self.missing_tags
279 |
280 | if val == MissingTags.strict:
281 | return True
282 | elif val == MissingTags.ignore:
283 | return False
284 |
285 | raise Exception("Unsupported 'missing_tags' value: %s" % repr(val))
286 |
287 | def _make_resolve_partial(self):
288 | """
289 | Return the resolve_partial function to pass to RenderEngine.__init__().
290 |
291 | """
292 | load_partial = self._make_load_partial()
293 |
294 | if self._is_missing_tags_strict():
295 | return load_partial
296 | # Otherwise, ignore missing tags.
297 |
298 | def resolve_partial(name):
299 | try:
300 | return load_partial(name)
301 | except TemplateNotFoundError:
302 | return ''
303 |
304 | return resolve_partial
305 |
306 | def _make_resolve_context(self):
307 | """
308 | Return the resolve_context function to pass to RenderEngine.__init__().
309 |
310 | """
311 | if self._is_missing_tags_strict():
312 | return context_get
313 | # Otherwise, ignore missing tags.
314 |
315 | def resolve_context(stack, name):
316 | try:
317 | return context_get(stack, name)
318 | except KeyNotFoundError:
319 | return ''
320 |
321 | return resolve_context
322 |
323 | def _make_render_engine(self):
324 | """
325 | Return a RenderEngine instance for rendering.
326 |
327 | """
328 | resolve_context = self._make_resolve_context()
329 | resolve_partial = self._make_resolve_partial()
330 |
331 | engine = RenderEngine(literal=self._to_unicode_hard,
332 | escape=self._escape_to_unicode,
333 | resolve_context=resolve_context,
334 | resolve_partial=resolve_partial,
335 | to_str=self.str_coerce)
336 | return engine
337 |
338 | # TODO: add unit tests for this method.
339 | def load_template(self, template_name):
340 | """
341 | Load a template by name from the file system.
342 |
343 | """
344 | load_template = self._make_load_template()
345 | return load_template(template_name)
346 |
347 | def _render_object(self, obj, *context, **kwargs):
348 | """
349 | Render the template associated with the given object.
350 |
351 | """
352 | loader = self._make_loader()
353 |
354 | # TODO: consider an approach that does not require using an if
355 | # block here. For example, perhaps this class's loader can be
356 | # a SpecLoader in all cases, and the SpecLoader instance can
357 | # check the object's type. Or perhaps Loader and SpecLoader
358 | # can be refactored to implement the same interface.
359 | if isinstance(obj, TemplateSpec):
360 | loader = SpecLoader(loader)
361 | template = loader.load(obj)
362 | else:
363 | template = loader.load_object(obj)
364 |
365 | context = [obj] + list(context)
366 |
367 | return self._render_string(template, *context, **kwargs)
368 |
369 | def render_name(self, template_name, *context, **kwargs):
370 | """
371 | Render the template with the given name using the given context.
372 |
373 | See the render() docstring for more information.
374 |
375 | """
376 | loader = self._make_loader()
377 | template = loader.load_name(template_name)
378 | return self._render_string(template, *context, **kwargs)
379 |
380 | def render_path(self, template_path, *context, **kwargs):
381 | """
382 | Render the template at the given path using the given context.
383 |
384 | Read the render() docstring for more information.
385 |
386 | """
387 | loader = self._make_loader()
388 | template = loader.read(template_path)
389 |
390 | return self._render_string(template, *context, **kwargs)
391 |
392 | def _render_string(self, template, *context, **kwargs):
393 | """
394 | Render the given template string using the given context.
395 |
396 | """
397 | # RenderEngine.render() requires that the template string be unicode.
398 | template = self._to_unicode_hard(template)
399 |
400 | render_func = lambda engine, stack: engine.render(template, stack)
401 |
402 | return self._render_final(render_func, *context, **kwargs)
403 |
404 | # All calls to render() should end here because it prepares the
405 | # context stack correctly.
406 | def _render_final(self, render_func, *context, **kwargs):
407 | """
408 | Arguments:
409 |
410 | render_func: a function that accepts a RenderEngine and ContextStack
411 | instance and returns a template rendering as a unicode string.
412 |
413 | """
414 | stack = ContextStack.create(*context, **kwargs)
415 | self._context = stack
416 |
417 | engine = self._make_render_engine()
418 |
419 | return render_func(engine, stack)
420 |
421 | def render(self, template, *context, **kwargs):
422 | """
423 | Render the given template string, view template, or parsed template.
424 |
425 | Returns a unicode string.
426 |
427 | Prior to rendering, this method will convert a template that is a
428 | byte string (type str in Python 2) to unicode using the string_encoding
429 | and decode_errors attributes. See the constructor docstring for
430 | more information.
431 |
432 | Arguments:
433 |
434 | template: a template string that is unicode or a byte string,
435 | a ParsedTemplate instance, or another object instance. In the
436 | final case, the function first looks for the template associated
437 | to the object by calling this class's get_associated_template()
438 | method. The rendering process also uses the passed object as
439 | the first element of the context stack when rendering.
440 |
441 | *context: zero or more dictionaries, ContextStack instances, or objects
442 | with which to populate the initial context stack. None
443 | arguments are skipped. Items in the *context list are added to
444 | the context stack in order so that later items in the argument
445 | list take precedence over earlier items.
446 |
447 | **kwargs: additional key-value data to add to the context stack.
448 | As these arguments appear after all items in the *context list,
449 | in the case of key conflicts these values take precedence over
450 | all items in the *context list.
451 |
452 | """
453 | if is_string(template):
454 | return self._render_string(template, *context, **kwargs)
455 | if isinstance(template, ParsedTemplate):
456 | render_func = lambda engine, stack: template.render(engine, stack)
457 | return self._render_final(render_func, *context, **kwargs)
458 | # Otherwise, we assume the template is an object.
459 |
460 | return self._render_object(template, *context, **kwargs)
461 |
462 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/test/appearing-text_result.sif:
--------------------------------------------------------------------------------
1 |
2 |
3 | Synfig Test for Keyframe import
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | part one
19 | part one - end
20 | part two
21 | part two - end
22 | part three
23 | part three - end
24 | part four
25 | part four - end
26 | part five
27 | part five - end
28 | part six
29 | part six - end
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 0.0000000000
43 | 0.0000000000
44 |
45 |
46 |
47 |
48 |
49 |
50 | 0.0000000000
51 | 0.0000000000
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 1.0000000000
63 | 1.0000000000
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | 1.000000
83 | 1.000000
84 | 1.000000
85 | 1.000000
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | this animation has been generated automatically
101 | from an Audacity Labels file, with SynfigStudio.
102 | All Open Source animation
103 |
104 |
105 |
106 | 0.000001
107 | 0.000001
108 | 0.000001
109 | 1.000000
110 |
111 |
112 |
113 | Sans Serif
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | 0.1333333403
130 | 0.1333333403
131 |
132 |
133 |
134 |
135 | 0.5000000000
136 | 0.5000000000
137 |
138 |
139 |
140 |
141 | 0.9415920973
142 | -1.7408642769
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 | part one
201 |
202 |
203 |
204 | 0.300000
205 | 0.000000
206 | 0.000000
207 | 1.000000
208 |
209 |
210 |
211 | Sans Serif
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 | 0.2500000000
228 | 0.2500000000
229 |
230 |
231 |
232 |
233 | 0.5000000000
234 | 0.5000000000
235 |
236 |
237 |
238 |
239 | 2.5868105888
240 | 0.5670760274
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | part two
278 |
279 |
280 |
281 | 0.300000
282 | 0.000000
283 | 0.000000
284 | 1.000000
285 |
286 |
287 |
288 | Sans Serif
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 | 0.2500000000
305 | 0.2500000000
306 |
307 |
308 |
309 |
310 | 0.5000000000
311 | 0.5000000000
312 |
313 |
314 |
315 |
316 | 0.4834947884
317 | -0.5177246332
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 | part three
355 |
356 |
357 |
358 | 0.300000
359 | 0.000000
360 | 0.000000
361 | 1.000000
362 |
363 |
364 |
365 | Sans Serif
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | 0.2500000000
382 | 0.2500000000
383 |
384 |
385 |
386 |
387 | 0.5000000000
388 | 0.5000000000
389 |
390 |
391 |
392 |
393 | -0.0666949674
394 | 1.1698374748
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 | part four
432 |
433 |
434 |
435 | 0.300000
436 | 0.000000
437 | 0.000000
438 | 1.000000
439 |
440 |
441 |
442 | Sans Serif
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 | 0.2500000000
459 | 0.2500000000
460 |
461 |
462 |
463 |
464 | 0.5000000000
465 | 0.5000000000
466 |
467 |
468 |
469 |
470 | -2.6029005051
471 | 0.9125182033
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 | part five
509 |
510 |
511 |
512 | 0.300000
513 | 0.000000
514 | 0.000000
515 | 1.000000
516 |
517 |
518 |
519 | Sans Serif
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 | 0.2500000000
536 | 0.2500000000
537 |
538 |
539 |
540 |
541 | 0.5000000000
542 | 0.5000000000
543 |
544 |
545 |
546 |
547 | 0.8887774348
548 | 0.7587298155
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 | part six
586 |
587 |
588 |
589 | 0.300000
590 | 0.000000
591 | 0.000000
592 | 1.000000
593 |
594 |
595 |
596 | Sans Serif
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 | 0.2500000000
613 | 0.2500000000
614 |
615 |
616 |
617 |
618 | 0.5000000000
619 | 0.5000000000
620 |
621 |
622 |
623 |
624 | -2.1670331955
625 | -0.1035533398
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
--------------------------------------------------------------------------------