Source code for quickly.lang.latex

# -*- coding: utf-8 -*-
# This file is part of `quickly`, a library for LilyPond and the `.ly` format
# Copyright © 2019-2020 by Wilbert Berendsen <>
# This module 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 3 of the License, or
# (at your option) any later version.
# This module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <>.

LaTeX language and transformation definition.

import itertools

from parce import skip, lexicon, default_target
from parce.rule import bygroup, ifarg, ifeq, ifgroup
import parce.lang.tex
import parce.action as a

from quickly.dom import base, element, lily, scm, tex
from . import lilypond

[docs]class Latex(parce.lang.tex.Latex): """Latex language definition."""
[docs] @classmethod def get_environment_target(cls, name): return ifeq(name, "lilypond", (lilypond.LilyPond.latex_lilypond_environment, cls.test_lilypond_option), super().get_environment_target(name))
[docs] @classmethod def common(cls): yield r'(\\lilypond)\s*(?:(\{)|(\[))?', bygroup(a.Name.Builtin, a.Delimiter.Brace, a.Delimiter), \ ifgroup(2, lilypond.LilyPond.latex_lilypond_environment('short form'), ifgroup(3, cls.option("lilypond"))) yield from super().common()
@lexicon def option(cls): yield ifarg(r'(\])\s*(\{)'), bygroup(a.Delimiter, a.Delimiter.Brace), -1, \ lilypond.LilyPond.latex_lilypond_environment('short form') yield from super().option yield r'\[', a.Delimiter.Bracket # this can match if we were here looking for a [ @lexicon def test_lilypond_option(cls): """One time check for Latex options at the beginning of a LilyPond environment. This lexicon never creates a context. """ yield r'(?=\s*\[)', skip, -1, cls.option yield default_target, -1
[docs]class LatexTransform(base.Transform): """Transform Latex quickly.dom.""" ## Transform methods
[docs] def root(self, items): """Process the ``root`` context.""" return tex.Document(*self.common(items))
[docs] def brace(self, items): """Process the ``brace`` context; returns a Brace node.""" head = items[:1] tail = (items.pop(),) if items[-1] == '}' else () return self.factory(tex.Brace, head, tail, *self.common(items[1:]))
[docs] def option(self, items): r"""Process the ``option`` context. Returns a two-tuple(options, head_origin). The options is a list of objects that are finished Option nodes, only the first might be incomplete because of a missing opening token. In that case the first object is a tuple (contents, tail_origin). (The tail_origin might be empty in the case of an imcomplete source text). The head_origin is normally empty, only for the ``\lilypond[opts]{`` command, it is the brace that starts the braced expression. """ command_head = () if items.peek(-1, a.Delimiter.Brace): # opening bracket of short form LilyPond command command_head = (items.pop(),) i = 0 if items.peek(0, a.Delimiter.Bracket) and items[0].text == '[': # complete first Option node head_origin = items[0:1] i = 1 else: head_origin = () nodes = [] pos = i while True: try: i = items.index(']', pos) except ValueError: nodes.append(self.factory(tex.Option, head_origin, (), *self.common(items[pos:]))) break tail_origin = items[i], if head_origin: # append complete node nodes.append(self.factory(tex.Option, head_origin, tail_origin, *self.common(items[pos:i]))) else: # append contents and tail origin, head is in a previous context nodes.append( (self.common(items[pos:i]), items[i:i+1]) ) pos = i + 1 # another Option node? try: i = items.index('[', pos) except ValueError: break pos = i + 1 head_origin = items[i:pos] return nodes, command_head
[docs] def environment_option(self, items): """Process the ``environment_option`` context. Returns a tuple(option, envname), where *option* is a list like the first value returned by :meth:`option` and *envname* an :class:`~quickly.dom.tex.EnvironmentName` node it it was there at the end of the options list, otherwise None. """ env_name = None if items.peek(-3, a.Delimiter, a.Name.Tag, a.Delimiter): # environment name at end env_name = self.factory(tex.EnvironmentName, items[-3:]) del items[-3:] return self.option(items)[0], env_name
[docs] def environment_math(self, items): """Process the ``environment_math`` context.""" return self.environment(items)
[docs] def environment(self, items): r"""Process the ``environment`` context. Returns a list of nodes, the last is the ``\end`` command. """ end = None if items.peek(-4, a.Name.Builtin, a.Delimiter, a.Name.Tag, a.Delimiter): end = self.factory(tex.Command, items[-4:-3]) end.append(self.factory(tex.EnvironmentName, items[-3:])) items = items[:-4] nodes = self.common(items) if end: nodes.append(end) return nodes
_math_mapping = element.head_mapping( tex.MathInlineParen, tex.MathInlineDollar, tex.MathDisplayBracket, tex.MathDisplayDollar)
[docs] def math(self, items): """Process the ``math`` context; return a Math node.""" head = items[:1] cls = self._math_mapping[head[0].text] tail = (items.pop(),) if len(items) > 1 and items[-1] == cls.tail else () return self.factory(cls, head, tail, *self.common(items[1:]))
[docs] def comment(self, items): """Process the ``comment`` context.""" return self.factory(tex.Comment, items)
test_lilypond_option = None # never creates content ## Helper methods
[docs] def common(self, items): """Compose and yield Latex nodes; used in most contexts.""" result = [] text = [] z = len(items) i = 0 def get_options(options): """Yield Option nodes; i must be at the ``[``.""" if isinstance(options[0], tuple): head = items[i:i+1] contents, tail = options[0] yield self.factory(tex.Option, head, tail, *contents) options = options[1:] yield from options def command(t): """Return a generic Command node. Arguments are appended as child.""" nonlocal i cmd = self.factory(tex.Command, (t,)) # options? append as child if items.peek(i, a.Delimiter.Bracket, "option"): cmd.extend(get_options(items[i+1].obj[0])) i += 2 # braced piece of text? append as child if items.peek(i, "brace"): cmd.append(items[i].obj) i += 1 return cmd def lilypond_command(t): r"""Return the Command node for a \lilypond { ... } command.""" nonlocal i cmd = self.factory(tex.Command, (t,)) if items.peek(i, a.Delimiter.Brace, 'latex_lilypond_environment'): options, music, tail = items[i+1].obj cmd.append(self.factory(tex.Brace, items[i:i+1], tail, *music)) i += 2 elif items.peek(i, a.Delimiter, 'option'): options, cmd_head = items[i+1].obj cmd.extend(get_options(options)) i += 2 if items.peek(i, 'latex_lilypond_environment'): options, music, tail = items[i].obj cmd.append(self.factory(tex.Brace, cmd_head, tail, *music)) i += 1 return cmd def environment(t): """Return an Environment node, possibly containing LilyPond music.""" nonlocal i env = tex.Environment(self.factory(tex.Command, (t,))) if items.peek(i, a.Delimiter, a.Name.Tag, a.Delimiter): # no options, add the name env[-1].append(self.factory(tex.EnvironmentName, items[i:i+3])) i += 3 elif items.peek(i, a.Delimiter.Bracket, "environment_option"): # environment options options, env_name = items[i+1].obj env[-1].extend(get_options(options)) if env_name: env[-1].append(env_name) i += 2 # now add the environment if i < z and not items[i].is_token: if items[i].name in ('environment', 'environment_math'): env.extend(items[i].obj) elif items[i].name == 'latex_lilypond_environment': options, music, tail = items[i].obj env[-1].extend(options) env.extend(music) else: print("unknown Latex environment:", items[i].name) #TEMP i += 1 return env while i < z: node = None t = items[i] i += 1 if t.is_token: if t.action is a.Name.Builtin: if t == r'\lilypond': node = lilypond_command(t) else: # if t -- r'\begin': node = environment(t) elif t.action is a.Name.Command: node = command(t) else: text.append(t) # t is a context result elif isinstance(t.obj, element.Element): node = t.obj else: print("unknown object:", # TEMP if node: if text: result.append(self.factory(tex.Text, text)) text = [] result.append(node) if text: result.append(self.factory(tex.Text, text)) return result
[docs]class LatexAdHocTransform(base.AdHocTransform, LatexTransform): """LatexTransform that does not keep the origin tokens.""" pass