├── .gitignore
├── ChangeLog
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs
├── Makefile
├── requirements.txt
└── source
│ ├── changelog.rst
│ ├── conf.py
│ ├── datatypes.rst
│ ├── dom.rst
│ ├── dom
│ ├── base.rst
│ ├── edit.rst
│ ├── element.rst
│ ├── htm.rst
│ ├── indent.rst
│ ├── lily.rst
│ ├── markup.rst
│ ├── read.rst
│ ├── scm.rst
│ ├── scope.rst
│ ├── tex.rst
│ └── util.rst
│ ├── duration.rst
│ ├── features.rst
│ ├── index.rst
│ ├── key.rst
│ ├── lang.rst
│ ├── lang
│ ├── html.rst
│ ├── latex.rst
│ ├── lilypond.rst
│ └── scheme.rst
│ ├── license.rst
│ ├── modoverview.rst
│ ├── node.rst
│ ├── numbering.rst
│ ├── overview.rst
│ ├── pitch.rst
│ ├── pkginfo.rst
│ ├── quick-ly.png
│ ├── quick-ly.svg
│ ├── quickly-screenshot.jpg
│ ├── quickly.rst
│ ├── quicklydom.rst
│ ├── relative.rst
│ ├── rhythm.rst
│ ├── time.rst
│ └── transpose.rst
├── quickly
├── __init__.py
├── datatypes.py
├── dom
│ ├── __init__.py
│ ├── base.py
│ ├── edit.py
│ ├── element.py
│ ├── htm.py
│ ├── indent.py
│ ├── lily.py
│ ├── markup.py
│ ├── read.py
│ ├── scm.py
│ ├── scope.py
│ ├── tex.py
│ └── util.py
├── duration.py
├── key.py
├── lang
│ ├── __init__.py
│ ├── html.py
│ ├── latex.py
│ ├── lilypond.py
│ └── scheme.py
├── node.py
├── numbering.py
├── pitch.py
├── pkginfo.py
├── relative.py
├── rhythm.py
├── time.py
└── transpose.py
├── setup.py
└── tests
├── test_dom.py
├── test_dom_edit.py
├── test_dom_util.py
├── test_duration.py
├── test_indent.py
├── test_key.py
├── test_node.py
├── test_numbering.py
├── test_pitch.py
├── test_rhythm.py
├── test_scheme.py
├── test_template.py
└── test_time.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/ChangeLog:
--------------------------------------------------------------------------------
1 | ChangeLog
2 | =========
3 |
4 |
5 | 2022-03-14: quickly-0.7.0
6 |
7 | - fix issue #3, allow adding octave to notes in chordmode, but not to the
8 | inversion
9 | - fix issue #4, relative to absolute yields incorrect results
10 | - removed the quickly.registry module, included languages are now added to
11 | parce's global registry
12 | - correctly handle \skip with music instead of a single duration (Lily>=2.23.6)
13 | - rhythm.transform() now also handles duration in commands like \tempo, \tuplet,
14 | \after and \partial
15 | - documentation improvements
16 |
17 |
18 | 2022-02-12: quickly-0.6.0
19 |
20 | - requires parce-0.31.0
21 | - small registry module changes to adapt to parce
22 |
23 |
24 | 2022-01-23: quickly-0.5.0
25 |
26 | - requires parce-0.30.0
27 | - added time module for time computations
28 | - added key module for key signature and tonic computations
29 | - added rhythm module to manipulate durations in music or a part thereof
30 | - added dom.scope module to find nodes from included files
31 | - modified PitchProcessor:
32 | - note names now carry a default octave
33 | - pitch() method now returns a Pitch, old pitch() renamed to process()
34 | - introduced lily.ChordBody and FigureBody, to be able to add Duration at Chord
35 | and Figure directly
36 | - added lily.Durable as base type for any stuff that can have a Duration
37 | - notes as argument to \relative, \key or \transpose are now Pitch, not Note
38 | - handle single durations in lyricmode, just like Unpitched
39 | - improvements to duration module, add duration.Transform
40 | - added transform() method to all Music nodes, to calculate durations in child
41 | nodes
42 | - added properties() method to Music node type, to store information during
43 | processing of descendant music
44 | - added datatypes module for small datatype helper classes, featuring Properties
45 | - documentation improvements
46 |
47 |
48 | 2021-12-31: quickly-0.4.0
49 |
50 | - added relative module, with abs2rel and rel2abs functions
51 | - possible to direct generators in Node and Range to not descend in a node
52 | - added node.Range class, to denote a range within a Node tree
53 | - added end parameter to Element.find_descendant(s)
54 | - added Element.find_descendant_right() and -left()
55 | - added dom.edit module to edit a parce document via the DOM node tree
56 | - added pitch.PitchProcessor.find_language and follow_language()
57 | - some methods/functions renamed and API cleanups
58 | - documentation improvements
59 |
60 |
61 | 2021-12-21: quickly-0.3.0
62 |
63 | - requires parce-0.28.0
64 | - DOM elegantly handles untransformed parce stuff such as CSS/JS in Html
65 | - Note and positioned Rest now have attrs for octave, accidental and oct_check
66 | - pitch: added Pitch and PitchProcessor to read/write pitch names in all
67 | languages, with style preferences for writing pitch names
68 | - added lily.Spanner.find_parallel() to find the other end of a spanner
69 | - added transpose module, transpose music in a node or (part of) parce.Document
70 |
71 |
72 | 2021-12-09: quickly-0.2.0
73 |
74 | - requires parce-0.27.0
75 | - added registry module to find and guess languages (like parce)
76 | - added LaTeX language and transform (for lilypond-book)
77 | - added element constructor helper for dom.htm module
78 | - added Element.py_dump() to create tree structures in Python code
79 | - simplified dom.read module and API, now that parce's Transform finding
80 | algorithm is configurable
81 | - small bugfixes, improvements and documentation updates
82 |
83 |
84 | 2021-11-25: quickly-0.1.0
85 |
86 | - requires parce-0.26.0
87 | - added html language and transform (for lilypond-book)
88 | - add dom.htm element types (for lilypond-book HTML documents)
89 | - added indent module and support for creating properly indented output
90 | - documentation improvements
91 |
92 |
93 | 2021-11-11: quickly-0.0.9
94 |
95 | - requires parce-0.25.0
96 | - Add support for some new LilyPond-2.23 commands
97 | - fixed case where a symbol was interpreted as a pitch eg: \tweak color red do
98 | - fixed some cases where incorrect input was not handled neatly
99 | - documentation improvements
100 |
101 |
102 | 2021-08-08: quickly-0.0.8
103 |
104 | - requires parce-0.24.0
105 | - fixed instance dict in Element nodes, added empty __slots__to mixins
106 | - added Node.filter() and Node.instances_of()
107 | - added a logo (two eighth rests, looking like "ly")
108 | - new website at https://quick-ly.info/
109 | - small optimizations and documentation improvements
110 |
111 |
112 | 2021-03-08: quickly-0.0.7
113 |
114 | - requires parce-0.23.0
115 | - added quickly.numbering module
116 | - added dom.read module to construct elements/documents from text
117 | - removed some temp methods from dom.util
118 | - Element.copy_with_origin now works properly everywhere
119 | - Node.equals() now works for TextElement, by implementing body_equals()
120 | - improvements to TextElement, optional head value checking
121 | - renamed lily.PropToggle to Toggle
122 | - fixed escape warnings in source files
123 | - added duration.shift_duration()
124 | - moved scm.Int to scm.Number in some forgotten places
125 |
126 |
127 | 2021-02-16: quickly-0.0.6
128 |
129 | - required parce version 0.22.0
130 | - lily.Header, lily.Paper and lily.Layout now have easy to use properties to
131 | set variables like title, composer, etc.
132 | - lily.Document now has version attribute to read/set LilyPond version
133 | - added markup construction helper in quickly.dom.markup
134 | - Scheme transform handles all Scheme number features, e.g. #xb/f (a fraction
135 | in hex) and even complex numbers
136 |
137 |
138 | 2021-02-11: quickly-0.0.5
139 |
140 | - Milestone: quickly correctly parses my Mouvement piece!
141 | https://github.com/wbsoft/lilymusic/blob/master/berendsen/mouvement.ly
142 | - fixed \rest before duration
143 | - fixed dot missing in scheme pair
144 | - add space after markup commands
145 | - fixed dom.element.build_tree, it lost a node after two \repeat nodes
146 | - parce removed identifier context; we only create Identifier with Assignment
147 | - smartly support \tag and \tweak without direction prepended
148 | - fixed documentation build errors.
149 |
150 |
151 | 2021-02-07: quickly-0.0.4
152 |
153 | - still pre-alpha but work in progress
154 | - quickly.dom.lily now has much more elements
155 | - correctly parse lists, property paths etc
156 | - added chordmode and chord modifiers, figuremode
157 | - quickly.lang.lilypond now transforms almost a full document
158 | - commands are combined with their arguments, based on signatures
159 | - work started on documentation with a quickly.dom explanation
160 |
161 |
162 | 2021-01-31: quickly-0.0.3
163 |
164 | - still pre-alpha
165 | - much more robust already
166 | - dom.element now has the four basic Element node types
167 | - dom.element can be constructed manually or via LilyPondTransform
168 | - writing back to document works
169 | - scheme expressions are fully read/built
170 | - markup expressions are fully read/built
171 | - complex articulations, tweak, tremolo etc. work well
172 | - toplevel and in block assignment is wrapped in Assignment node
173 | - lyricmode and drummode work
174 | - todo: music functions
175 | - some test files were added. todo: more test files
176 | - module documentation in good shape. todo: user documentation
177 |
178 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | global-exclude *~
2 | include README.rst ChangeLog LICENSE
3 | graft docs
4 | prune docs/build
5 | recursive-include tests *.py
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Welcome to quickly!
2 | ===================
3 |
4 | `Homepage `_ •
5 | `Development `_ •
6 | `Download `_ •
7 | `Documentation `_ •
8 | `License `_
9 |
10 | The *quickly* python package is able to create and manipulate LilyPond music
11 | documents. LilyPond documents often use the ``.ly`` extension, hence the name.
12 |
13 | It is currently in an early development stage, but slated to become the
14 | successor of the `python-ly`_ package. Like python-ly, it provides tools to
15 | manipulate `LilyPond`_ music documents, but instead of using the lexer in
16 | python-ly, which is very difficult to maintain, it uses the new `parce`_
17 | package for parsing `LilyPond`_ files.
18 |
19 | ``ly.dom`` and ``ly.music`` are superseded by ``quickly.dom`` which provides a
20 | way to both build a LilyPond source file from scratch (like ``ly.dom``) and
21 | manipulate an existing document (like ``ly.music``). Most of the functionality
22 | that in python-ly is implemented at the token level, like transposing and
23 | rhythm manipulations, now works on the musical representation provided by
24 | ``quickly.dom``, which looks a lot like ``ly.music``.
25 |
26 | This module is written and maintained by Wilbert Berendsen, and will, as it
27 | grows, also contain (adapted) code from python-ly that was contributed by
28 | others. Python 3.6 and higher is supported. Besides Python itself the most
29 | recent version of the *parce* module is needed. Testing is done by running
30 | ``pytest-3`` in the root directory.
31 |
32 | The documentation reflects which parts are already working :-) Enjoy!
33 |
34 | .. _python-ly: https://github.com/frescobaldi/python-ly/
35 | .. _LilyPond: http://lilypond.org/
36 | .. _parce: https://parce.info/
37 |
38 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = quickly
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Upload (added by WB)
18 | upload: html
19 | rsync -ave ssh --exclude '*~' \
20 | build/html/* \
21 | wilbertb@wilbertberendsen.nl:/home/wilbertb/domains/quick-ly.info/public_html/
22 |
23 | # Catch-all target: route all unknown targets to Sphinx using the new
24 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
25 | %: Makefile
26 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
27 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | parce >= 0.21
2 |
--------------------------------------------------------------------------------
/docs/source/changelog.rst:
--------------------------------------------------------------------------------
1 | :orphan:
2 |
3 | .. include:: ../../ChangeLog
4 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # *quickly* documentation build configuration file.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | import os
20 | import sys
21 | sys.path.insert(0, os.path.abspath('../..'))
22 | import quickly.pkginfo
23 |
24 |
25 | # -- General configuration ------------------------------------------------
26 |
27 | # If your documentation needs a minimal Sphinx version, state it here.
28 | #
29 | # needs_sphinx = '1.0'
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = [
35 | 'sphinx.ext.autodoc',
36 | # 'sphinx.ext.doctest',
37 | # 'sphinx.ext.coverage',
38 | 'sphinx.ext.viewcode',
39 | 'sphinx.ext.intersphinx',
40 | ]
41 |
42 | # autodoc
43 | autodoc_member_order = 'bysource'
44 | autodoc_default_options = {
45 | 'member-order': 'bysource',
46 | }
47 |
48 | # If true, the current module name will be prepended to all description
49 | # unit titles (such as .. function::).
50 | add_module_names = False
51 |
52 | # intersphinx
53 | intersphinx_mapping = {
54 | 'python': ('https://docs.python.org/3', None),
55 | 'parce': ('https://parce.info', None),
56 | }
57 |
58 | # Add any paths that contain templates here, relative to this directory.
59 | templates_path = ['_templates']
60 |
61 | # The suffix(es) of source filenames.
62 | # You can specify multiple suffix as a list of string:
63 | #
64 | # source_suffix = ['.rst', '.md']
65 | source_suffix = '.rst'
66 |
67 | # The master toctree document.
68 | master_doc = 'index'
69 |
70 | # General information about the project.
71 | project = quickly.pkginfo.name
72 | copyright = quickly.pkginfo.copyright_year + ', ' + quickly.pkginfo.maintainer
73 | author = quickly.pkginfo.maintainer
74 |
75 | # The version info for the project you're documenting, acts as replacement for
76 | # |version| and |release|, also used in various other places throughout the
77 | # built documents.
78 | #
79 | # The short X.Y version.
80 | version = '.'.join(map(format, quickly.version))
81 | # The full version, including alpha/beta/rc tags.
82 | release = quickly.pkginfo.version_string
83 |
84 | # The language for content autogenerated by Sphinx. Refer to documentation
85 | # for a list of supported languages.
86 | #
87 | # This is also used if you do content translation via gettext catalogs.
88 | # Usually you set "language" from the command line for these cases.
89 | language = None
90 |
91 | # List of patterns, relative to source directory, that match files and
92 | # directories to ignore when looking for source files.
93 | # This patterns also effect to html_static_path and html_extra_path
94 | exclude_patterns = []
95 |
96 | # The name of the Pygments (syntax highlighting) style to use.
97 | pygments_style = 'sphinx'
98 |
99 | # If true, `todo` and `todoList` produce output, else they produce nothing.
100 | todo_include_todos = False
101 |
102 |
103 | # -- Options for HTML output ----------------------------------------------
104 |
105 | # The theme to use for HTML and HTML Help pages. See the documentation for
106 | # a list of builtin themes.
107 | #
108 | #html_theme = 'alabaster'
109 | html_theme = "sphinx_rtd_theme"
110 |
111 | # Theme options are theme-specific and customize the look and feel of a theme
112 | # further. For a list of options available for each theme, see the
113 | # documentation.
114 | #
115 | html_theme_options = {
116 | #'canonical_url': 'https://quick-ly.info/',
117 | }
118 |
119 | html_favicon = "quick-ly.png"
120 | html_logo = "quick-ly.png"
121 |
122 | # Add any paths that contain custom static files (such as style sheets) here,
123 | # relative to this directory. They are copied after the builtin static files,
124 | # so a file named "default.css" will overwrite the builtin "default.css".
125 | html_static_path = ['_static']
126 |
127 | # Custom sidebar templates, must be a dictionary that maps document names
128 | # to template names.
129 | #
130 | # This is required for the alabaster theme
131 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
132 | html_sidebars = {
133 | '**': [
134 | 'about.html',
135 | 'navigation.html',
136 | 'relations.html', # needs 'show_related': True theme option to display
137 | 'searchbox.html',
138 | ]
139 | }
140 |
141 |
142 | # -- Options for HTMLHelp output ------------------------------------------
143 |
144 | # Output file base name for HTML help builder.
145 | htmlhelp_basename = 'quicklydoc'
146 |
147 |
148 | # -- Options for LaTeX output ---------------------------------------------
149 |
150 | latex_elements = {
151 | # The paper size ('letterpaper' or 'a4paper').
152 | #
153 | # 'papersize': 'letterpaper',
154 |
155 | # The font size ('10pt', '11pt' or '12pt').
156 | #
157 | # 'pointsize': '10pt',
158 |
159 | # Additional stuff for the LaTeX preamble.
160 | #
161 | # 'preamble': '',
162 |
163 | # Latex figure (float) alignment
164 | #
165 | # 'figure_align': 'htbp',
166 | }
167 |
168 | # Grouping the document tree into LaTeX files. List of tuples
169 | # (source start file, target name, title,
170 | # author, documentclass [howto, manual, or own class]).
171 | latex_documents = [
172 | (master_doc, 'quickly.tex', 'quickly Documentation',
173 | 'Wilbert Berendsen', 'manual'),
174 | ]
175 |
176 |
177 | # -- Options for manual page output ---------------------------------------
178 |
179 | # One entry per manual page. List of tuples
180 | # (source start file, name, description, authors, manual section).
181 | man_pages = [
182 | (master_doc, 'quickly', 'quickly Documentation',
183 | [author], 1)
184 | ]
185 |
186 |
187 | # -- Options for Texinfo output -------------------------------------------
188 |
189 | # Grouping the document tree into Texinfo files. List of tuples
190 | # (source start file, target name, title, author,
191 | # dir menu entry, description, category)
192 | texinfo_documents = [
193 | (master_doc, 'quickly', 'quickly Documentation',
194 | author, 'quickly', 'One line description of project.',
195 | 'Miscellaneous'),
196 | ]
197 |
198 |
199 |
200 |
--------------------------------------------------------------------------------
/docs/source/datatypes.rst:
--------------------------------------------------------------------------------
1 | The datatypes module
2 | ====================
3 |
4 | .. automodule:: quickly.datatypes
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom.rst:
--------------------------------------------------------------------------------
1 | The dom package
2 | ===============
3 |
4 | .. automodule:: quickly.dom
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 | Modules in the ``dom`` package:
10 |
11 | .. toctree::
12 | :maxdepth: 1
13 |
14 | dom/edit.rst
15 | dom/element.rst
16 | dom/base.rst
17 | dom/htm.rst
18 | dom/lily.rst
19 | dom/scm.rst
20 | dom/tex.rst
21 | dom/markup.rst
22 | dom/read.rst
23 | dom/scope.rst
24 | dom/indent.rst
25 | dom/util.rst
26 |
--------------------------------------------------------------------------------
/docs/source/dom/base.rst:
--------------------------------------------------------------------------------
1 | The dom.base module
2 | ===================
3 |
4 | .. automodule:: quickly.dom.base
5 |
6 |
7 | Base classes
8 | ------------
9 |
10 | The following are base element classes for different languages.
11 |
12 | .. autoclass:: Document
13 | :members:
14 | :undoc-members:
15 | :show-inheritance:
16 |
17 | .. autoclass:: String
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
22 | .. autoclass:: Comment
23 | :members:
24 | :undoc-members:
25 | :show-inheritance:
26 |
27 | .. autoclass:: SinglelineComment
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
32 | .. autoclass:: MultilineComment
33 | :members:
34 | :undoc-members:
35 | :show-inheritance:
36 |
37 | .. autoclass:: BackslashCommand
38 | :members:
39 | :undoc-members:
40 | :show-inheritance:
41 |
42 |
43 | Generic elements
44 | ----------------
45 |
46 | These are generic elements, which are never created from transforming a
47 | sourcce, but may be used to alter the output of a DOM tree/document.
48 |
49 | .. autoclass:: Newline
50 | :members:
51 | :undoc-members:
52 | :show-inheritance:
53 |
54 | .. autoclass:: BlankLine
55 | :members:
56 | :undoc-members:
57 | :show-inheritance:
58 |
59 | .. autoclass:: Line
60 | :members:
61 | :undoc-members:
62 | :show-inheritance:
63 |
64 | .. autoclass:: Column
65 | :members:
66 | :undoc-members:
67 | :show-inheritance:
68 |
69 | .. autoclass:: Text
70 | :members:
71 | :undoc-members:
72 | :show-inheritance:
73 |
74 |
75 | Special element
76 | ---------------
77 |
78 | There is one "special" element.
79 |
80 | .. autoclass:: Unknown
81 | :members:
82 | :undoc-members:
83 | :show-inheritance:
84 |
85 |
86 | Language and Transform base/helper classes
87 | ------------------------------------------
88 |
89 | .. autoclass:: XmlLike
90 | :members:
91 | :undoc-members:
92 | :show-inheritance:
93 |
94 | .. autoclass:: Transform
95 | :members:
96 | :undoc-members:
97 | :show-inheritance:
98 |
99 | .. autoclass:: AdHocTransform
100 | :members:
101 | :undoc-members:
102 | :show-inheritance:
103 |
104 |
--------------------------------------------------------------------------------
/docs/source/dom/edit.rst:
--------------------------------------------------------------------------------
1 | The dom.edit module
2 | ===================
3 |
4 | .. automodule:: quickly.dom.edit
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/docs/source/dom/element.rst:
--------------------------------------------------------------------------------
1 | The dom.element module
2 | ======================
3 |
4 | .. automodule:: quickly.dom.element
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/htm.rst:
--------------------------------------------------------------------------------
1 | The dom.htm module
2 | ==================
3 |
4 | .. automodule:: quickly.dom.htm
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/indent.rst:
--------------------------------------------------------------------------------
1 | The dom.indent module
2 | =====================
3 |
4 | .. automodule:: quickly.dom.indent
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/lily.rst:
--------------------------------------------------------------------------------
1 | The dom.lily module
2 | ===================
3 |
4 | .. automodule:: quickly.dom.lily
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/markup.rst:
--------------------------------------------------------------------------------
1 | The dom.markup module
2 | =====================
3 |
4 | .. automodule:: quickly.dom.markup
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 | :member-order: alphabetical
9 |
--------------------------------------------------------------------------------
/docs/source/dom/read.rst:
--------------------------------------------------------------------------------
1 | The dom.read module
2 | ===================
3 |
4 | .. automodule:: quickly.dom.read
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/scm.rst:
--------------------------------------------------------------------------------
1 | The dom.scm module
2 | ==================
3 |
4 | .. automodule:: quickly.dom.scm
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/scope.rst:
--------------------------------------------------------------------------------
1 | The dom.scope module
2 | ====================
3 |
4 | .. automodule:: quickly.dom.scope
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/docs/source/dom/tex.rst:
--------------------------------------------------------------------------------
1 | The dom.tex module
2 | ==================
3 |
4 | .. automodule:: quickly.dom.tex
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/dom/util.rst:
--------------------------------------------------------------------------------
1 | The dom.util module
2 | ===================
3 |
4 | .. automodule:: quickly.dom.util
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/duration.rst:
--------------------------------------------------------------------------------
1 | The duration module
2 | ===================
3 |
4 | .. automodule:: quickly.duration
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/features.rst:
--------------------------------------------------------------------------------
1 | Features of quickly
2 | ===================
3 |
4 | Supported document types
5 | ------------------------
6 |
7 | *quickly* supports LilyPond, Html, LaTeX and Scheme documents. DocBook and
8 | TexInfo are in the works.
9 |
10 | Html, LaTeX, DocBook and TexInfo are used to edit documents for
11 | ``lilypond-book``, a script provided by the `LilyPond`_ project that can extract
12 | fragments of LilyPond music and run other document processors to produce output
13 | of the texts with the musical fragments properly inserted. *quickly* is able to
14 | recognize LilyPond music inside these document formats and allows the user to
15 | manipulate the music.
16 |
17 | Importing ``quickly`` adds those language definitions to the *parce* registry.
18 | Most language definitions included in ``quickly.lang`` inherit from the
19 | language defitions with the same name from *parce*.
20 |
21 | So, after importing ``quickly``, :func:`parce.find` will find the language
22 | definitions of *quickly*::
23 |
24 | >>> import parce
25 | >>> parce.find('lilypond').language
26 | parce.lang.lilypond.LilyPond
27 | >>> import quickly
28 | >>> parce.find('lilypond').language
29 | quickly.lang.lilypond.LilyPond
30 |
31 |
32 | .. _LilyPond: http://lilypond.org/
33 |
34 | Music manipulations
35 | -------------------
36 |
37 | Most of *quickly*'s features manipulate music on the DOM document level,
38 | writing back the modifications to the originating text, without interfering
39 | with other parts of the source text.
40 |
41 | There are four "levels" at which documents can be manipulated: a DOM node tree,
42 | a :class:`~quickly.node.Range` of a DOM tree, a *parce* :class:`~parce.Document`, or a
43 | selection of a parce document, specified by a *parce* :class:`~parce.Cursor`.
44 |
45 | All manipulation functions can handle all four levels of operation, because
46 | they inherit :class:`~quickly.dom.edit.Edit`. Typically only the
47 | :meth:`~quickly.dom.edit.Edit.edit_range` method needs to be implemented for
48 | the others to work equally well. Most modules have convenience functions that
49 | can be called with all four types.
50 |
51 |
52 | Transpose
53 | ^^^^^^^^^
54 |
55 | .. currentmodule:: quickly.transpose
56 |
57 | Transposing music is done using the :mod:`.transpose` module. A
58 | Transposer is created that can actually transpose pitches according to the
59 | user's wish, and optionally a PitchProcessor that reads and writes LilyPond
60 | pitch names in all languages.
61 |
62 | An example; create a document::
63 |
64 | >>> import parce, quickly
65 | >>> doc = parce.Document(parce.find("lilypond"), r"music = { c d e f g }", transformer=True)
66 |
67 | Create a transposer::
68 |
69 | >>> from quickly.pitch import Pitch
70 | >>> from quickly.transpose import transpose, Transposer
71 | >>> p1 = Pitch(-1, 0, 0) # -> c
72 | >>> p2 = Pitch(-1, 3, 0) # -> f
73 | >>> t = Transposer(p1, p2)
74 |
75 | Now transpose the music from ``c`` to ``f`` and view the result::
76 |
77 | >>> transpose(doc, t)
78 | >>> doc.text()
79 | "music = { f g a bes c' }"
80 |
81 | Using the cursor, we can also operate on a fragment of the document::
82 |
83 | >>> cur = parce.Cursor(doc, 12, 15) # only the second and third note
84 | >>> transpose(cur, t)
85 | >>> doc.text()
86 | "music = { f c' d' bes c' }"
87 |
88 | Only the second and third note are transposed. The function :func:`transpose`
89 | is a convenience function that creates a :class:`Transpose` object and calls
90 | its :meth:`~quickly.dom.edit.Edit.edit` method.
91 |
92 |
93 | Convert pitches to and from relative notation
94 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
95 |
96 | .. currentmodule:: quickly.relative
97 |
98 | The :mod:`.relative` module contains functions to convert music to and
99 | from relative notation. These functions also use the PitchProcessor to read and
100 | write pitch names in all languages, and automatically adapt to the pitch
101 | language used in a document.
102 |
103 | To convert all music from relative to absolute notation::
104 |
105 | >>> import parce
106 | >>> from quickly import find
107 | >>> doc = parce.Document(find("lilypond"), r"music = \relative c' { c d e f g }", transformer=True)
108 | >>> from quickly.relative import rel2abs
109 | >>> rel2abs(doc)
110 | >>> doc.text()
111 | "music = { c' d' e' f' g' }"
112 |
113 | And convert back to relative::
114 |
115 | >>> from quickly.relative import abs2rel
116 | >>> abs2rel(doc)
117 | >>> doc.text()
118 | "music = \\relative c' { c d e f g }"
119 |
120 | The function :func:`abs2rel` and :func:`rel2abs` are convenience functions that
121 | create respectively a :class:`Abs2rel` or :class:`Rel2abs` object and call
122 | their :meth:`~quickly.dom.edit.Edit.edit` method.
123 |
124 |
125 | Time and rhythm
126 | ^^^^^^^^^^^^^^^
127 |
128 | .. currentmodule:: quickly.rhythm
129 |
130 | The :mod:`.rhythm` module provides easy-to-use functions and classes to modify
131 | the durations of music. Nodes that have a duration (such as notes, rests,
132 | spacers, skips, drum notes but also lyric words) always inherit
133 | :class:`~.dom.lily.Durable`, and can have a :class:`~.dom.lily.Duration` child
134 | node that writes the duration. Durable nodes also have convenient attributes
135 | :attr:`~.dom.lily.Durable.duration` and :attr:`~.dom.lily.Durable.scaling` to
136 | manipulate their Duration and DurationScaling child nodes.
137 |
138 | That makes it not too complicated to build nice functions editing these nodes,
139 | that are used to refactor or modify existing music:
140 |
141 | .. list-table::
142 |
143 | * - :func:`remove`
144 | - remove all durations from music
145 | * - :func:`remove_scaling`
146 | - remove scaling e.g. (``*2`` or ``*1/3``) from all durations
147 | * - :func:`remove_fraction_scaling`
148 | - remove only scaling that contains a fraction from the durations
149 | * - :func:`explicit`
150 | - write the duration after all notes, rests etc
151 | * - :func:`implicit`
152 | - only write the duration if different from the previous
153 | * - :func:`transform`
154 | - modify duration log, number of dots and/or scaling
155 | * - :func:`copy`
156 | - extract durations to a list of (duration, scaling) tuples
157 | * - :func:`paste`
158 | - overwrite durations in music from a list of (duration, scaling) tuples
159 |
160 | There is also the :mod:`.time` module, which provides functions to compute the
161 | length of musical fragments, or to compute the musical position a text cursor
162 | is at. Low level duration logic is in the :mod:`.duration` module.
163 |
164 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../README.rst
2 |
3 | .. figure:: quickly-screenshot.jpg
4 |
5 | A screenshot of Frescobaldi (which does not yet use *quickly*), the
6 | *parceqt* debugger and a terminal window showing a dump of the *quickly* DOM
7 | tree of a small piece of music.
8 |
9 | This manual documents `quickly` version |release|.
10 | Last update: |today|.
11 |
12 | .. toctree::
13 | :maxdepth: 2
14 | :caption: Contents:
15 |
16 | overview.rst
17 | features.rst
18 | quicklydom.rst
19 | modoverview.rst
20 |
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :doc:`changelog`
28 | * :doc:`license`
29 |
--------------------------------------------------------------------------------
/docs/source/key.rst:
--------------------------------------------------------------------------------
1 | The key module
2 | ==============
3 |
4 | .. automodule:: quickly.key
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/lang.rst:
--------------------------------------------------------------------------------
1 | The lang package
2 | ================
3 |
4 | .. automodule:: quickly.lang
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 | Modules in the ``lang`` package:
10 |
11 | .. toctree::
12 | :maxdepth: 1
13 |
14 | lang/html.rst
15 | lang/latex.rst
16 | lang/lilypond.rst
17 | lang/scheme.rst
18 |
19 |
--------------------------------------------------------------------------------
/docs/source/lang/html.rst:
--------------------------------------------------------------------------------
1 | The lang.html module
2 | ====================
3 |
4 | .. automodule:: quickly.lang.html
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/lang/latex.rst:
--------------------------------------------------------------------------------
1 | The lang.latex module
2 | =====================
3 |
4 | .. automodule:: quickly.lang.latex
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/lang/lilypond.rst:
--------------------------------------------------------------------------------
1 | The lang.lilypond module
2 | ========================
3 |
4 | .. automodule:: quickly.lang.lilypond
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/lang/scheme.rst:
--------------------------------------------------------------------------------
1 | The lang.scheme module
2 | ======================
3 |
4 | .. automodule:: quickly.lang.scheme
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/license.rst:
--------------------------------------------------------------------------------
1 | :orphan:
2 |
3 | License
4 | =======
5 |
6 | The *quickly* package is licensed under the General Public License v3.
7 |
8 | .. include:: ../../LICENSE
9 |
--------------------------------------------------------------------------------
/docs/source/modoverview.rst:
--------------------------------------------------------------------------------
1 | Overview of all modules
2 | =======================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | quickly.rst
8 | datatypes.rst
9 | dom.rst
10 | duration.rst
11 | key.rst
12 | lang.rst
13 | node.rst
14 | numbering.rst
15 | pitch.rst
16 | pkginfo.rst
17 | relative.rst
18 | rhythm.rst
19 | time.rst
20 | transpose.rst
21 |
22 |
--------------------------------------------------------------------------------
/docs/source/node.rst:
--------------------------------------------------------------------------------
1 | The node module
2 | ===============
3 |
4 | .. automodule:: quickly.node
5 | :members:
6 | :special-members: __contains__, __bool__
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/numbering.rst:
--------------------------------------------------------------------------------
1 | The numbering module
2 | ====================
3 |
4 | .. automodule:: quickly.numbering
5 | :members:
6 | :show-inheritance:
7 |
8 |
--------------------------------------------------------------------------------
/docs/source/overview.rst:
--------------------------------------------------------------------------------
1 | Overview
2 | ========
3 |
4 | The *quickly* module provides ready-to-use functions to create, manipulate and
5 | convert `LilyPond`_ music text documents, and the building blocks to create new
6 | functionality.
7 |
8 | Besides Python and its standard library, *quickly* only depends on `parce`_.
9 | LilyPond documents are plain text documents; using *parce*, the text is
10 | tokenized into a tree structure of parce tokens and contexts, and then
11 | transformed into a "Document Object Model", a more semantical tree structure of
12 | :mod:`~quickly.dom` nodes.
13 |
14 | .. _parce: https://parce.info/
15 | .. _LilyPond: http://lilypond.org/
16 |
17 | When a document is modified (e.g. by the user, typing in a text editor), the
18 | tokens and the DOM document are automatically updated.
19 |
20 | The two cornerstones of *quickly* are the :class:`parce.Document` (or any class
21 | following this interface), and the *quickly* DOM. Lexing the text in the
22 | document is done by *parce*, using a root lexicon, which belongs to a language
23 | definition. Transforming the lexed text into a DOM document is also done by
24 | *parce*, using a :class:`~parce.transform.Transform` that's coupled to the
25 | language definition.
26 |
27 | Most music manipulation functions operate on the *quickly* DOM, which
28 | afterwards can update the text document it originated from, if desired. To
29 | target only specific regions in a text document, often a :class:`parce.Cursor`
30 | is used.
31 |
32 | To create a parce Document, with LilyPond contents::
33 |
34 | >>> import parce, quickly
35 | >>> doc = parce.Document(parce.find("lilypond"), transformer=True)
36 | >>> doc.set_text(r"music = { c d e f g }")
37 |
38 | To get the transformed DOM document:
39 |
40 | >>> music = doc.get_transform(True)
41 | >>> music.dump()
42 |
43 | ╰╴
44 | ├╴
45 | │ ╰╴
46 | ├╴
47 | ╰╴
48 | ├╴
49 | ├╴
50 | ├╴
51 | ├╴
52 | ╰╴
53 |
54 | See :doc:`quicklydom` for more information about the DOM used by *quickly*.
55 |
56 |
--------------------------------------------------------------------------------
/docs/source/pitch.rst:
--------------------------------------------------------------------------------
1 | The pitch module
2 | ================
3 |
4 | .. automodule:: quickly.pitch
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/docs/source/pkginfo.rst:
--------------------------------------------------------------------------------
1 | The pkginfo module
2 | ==================
3 |
4 | .. automodule:: quickly.pkginfo
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/docs/source/quick-ly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frescobaldi/quickly/79fa75acc85397b580c85ae0497a7ae7c25a0dbe/docs/source/quick-ly.png
--------------------------------------------------------------------------------
/docs/source/quick-ly.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
22 |
45 |
47 |
48 |
50 | image/svg+xml
51 |
53 |
54 |
55 |
56 |
57 |
62 |
76 |
91 |
94 |
105 |
110 |
111 |
122 |
127 |
128 |
129 |
143 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/docs/source/quickly-screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frescobaldi/quickly/79fa75acc85397b580c85ae0497a7ae7c25a0dbe/docs/source/quickly-screenshot.jpg
--------------------------------------------------------------------------------
/docs/source/quickly.rst:
--------------------------------------------------------------------------------
1 | The main quickly module
2 | =======================
3 |
4 | .. automodule:: quickly
5 | :members:
6 | :show-inheritance:
7 |
8 | .. py:data:: version
9 |
10 | The version as a three-tuple(major, minor, patch). See :mod:`~quickly.pkginfo`.
11 |
12 | .. py:data:: version_string
13 |
14 | The version as a string.
15 |
--------------------------------------------------------------------------------
/docs/source/relative.rst:
--------------------------------------------------------------------------------
1 | The relative module
2 | ====================
3 |
4 | .. automodule:: quickly.relative
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/docs/source/rhythm.rst:
--------------------------------------------------------------------------------
1 | The rhythm module
2 | =================
3 |
4 | .. automodule:: quickly.rhythm
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/time.rst:
--------------------------------------------------------------------------------
1 | The time module
2 | ===============
3 |
4 | .. automodule:: quickly.time
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/transpose.rst:
--------------------------------------------------------------------------------
1 | The transpose module
2 | ====================
3 |
4 | .. automodule:: quickly.transpose
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
--------------------------------------------------------------------------------
/quickly/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | The quickly module.
23 |
24 | On first import, our own language definitions are added to the parce registry.
25 |
26 | """
27 |
28 | import os.path
29 |
30 | from parce import find, Document
31 |
32 | from .pkginfo import version, version_string
33 |
34 |
35 | __all__ = ('find', 'load', 'version', 'version_string')
36 |
37 |
38 | def load(filename, lexicon=True, encoding=None, errors=None, newline=None):
39 | """Convenience function to read text from ``filename`` and return a
40 | :class:`parce.Document`.
41 |
42 | If ``lexicon`` is True, the lexicon will be guessed based on filename and
43 | contents. If it is a string name, its name is looked up in the registry;
44 | otherwise the lexicon is used directly.
45 |
46 | The ``encoding``, if specified, is used to read the file; otherwise the
47 | encoding is autodetected. The ``errors`` and ``newline`` arguments will be
48 | passed to Python's :func:`open` function. Raises :class:`OSError` if the
49 | file can't be read.
50 |
51 | """
52 | return Document.load(os.path.abspath(filename), lexicon, encoding, errors, newline, transformer=True)
53 |
54 |
55 | ## register bundled languages in quickly here
56 | from parce.registry import register
57 | register("quickly.lang.html.Html.root",
58 | inherit = "parce.lang.html.Html.root",
59 | name = "HTML",
60 | desc = "HTML with embedded LilyPond",
61 | )
62 |
63 | register("quickly.lang.latex.Latex.root",
64 | inherit = "parce.lang.tex.Latex.root",
65 | name = "LaTeX",
66 | desc = "LaTeX with embedded LilyPond",
67 | filenames = [("*.lytex", 1)],
68 | )
69 |
70 | register("quickly.lang.lilypond.LilyPond.root",
71 | inherit = "parce.lang.lilypond.LilyPond.root",
72 | )
73 |
74 | register("quickly.lang.scheme.Scheme.root",
75 | inherit = "parce.lang.scheme.Scheme.root",
76 | )
77 |
78 | #TODO: texinfo and docbook
79 | del register
80 |
81 |
--------------------------------------------------------------------------------
/quickly/datatypes.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2022 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Some generic datatypes used by quickly.
23 |
24 | Too small to justify separate modules but too generic to be added to some
25 | module where they are actually used.
26 |
27 | """
28 |
29 | class Properties:
30 | """A dictionary-like object that accesses keys as attributes.
31 |
32 | Adding another Properties object returns a new Properties instance
33 | with updated dict contents. Example::
34 |
35 | >>> from quickly.datatypes import Properties
36 | >>> p = Properties(repeat_count=3)
37 | >>> p
38 |
39 | >>> p.repeat_count
40 | 3
41 | >>> p1 = Properties(unfold=True)
42 | >>> p + p1
43 |
44 | >>> del p.repeat_count
45 | >>> p
46 |
47 |
48 | Accessing a non-existent property name returns None. Deleting a
49 | non-existent property does not raise an AttributeError. Setting an
50 | attribute to None does keep the attribute, and when adding the properties
51 | to another, the same property in the other will be overwritten by the newer
52 | one that was set to None.
53 |
54 | Use :func:`vars` to get a dictionary view on the properties. Use ``"key" in
55 | props`` to see whether an attribute is really present. An empty Properties
56 | object evaluates to False.
57 |
58 | """
59 | def __init__(self, **kwargs):
60 | self.__dict__.update(kwargs)
61 |
62 | def __bool__(self):
63 | return bool(vars(self))
64 |
65 | def __repr__(self):
66 | def fields():
67 | yield type(self).__name__
68 | yield " ".join(("{}={}".format(
69 | name, repr(value)) for name, value in vars(self).items()))
70 | return "<{}>".format(" ".join(f for f in fields() if f))
71 |
72 | def __getattr__(self, name):
73 | return None
74 |
75 | def __delattr__(self, name):
76 | try:
77 | del self.__dict__[name]
78 | except KeyError:
79 | pass
80 |
81 | def __eq__(self, other):
82 | if isinstance(other, Properties):
83 | return vars(self) == vars(other)
84 | return NotImplemented
85 |
86 | def __contains__(self, name):
87 | return name in self.__dict__
88 |
89 | def __add__(self, other):
90 | d = vars(self) | vars(other)
91 | return type(self)(**d)
92 |
93 |
94 |
--------------------------------------------------------------------------------
/quickly/dom/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | This module defines a DOM (Document Object Model) for LilyPond source files.
23 |
24 | This LilyPond DOM is a simple tree structure where a command or environment
25 | is represented by a node with possible child nodes.
26 |
27 | Some LilyPond commands have their own node type, and the arguments are
28 | represented by child nodes, while other commands use a generic Command node.
29 |
30 | This DOM is used in two ways:
31 |
32 | 1. Building a LilyPond source document from scratch. This helps to create
33 | a LilyPond document, although it in no way forces the output to be a valid
34 | LilyPond file.
35 |
36 | 2. Transforming a *parce* tree of an existing LilyPond source document to a DOM
37 | document. All tokens are stored in the nodes (in the ``origin`` attributes),
38 | so it is possible to write back modifications to the original document
39 | without touching other parts of the document.
40 |
41 | All nodes in this DOM inherit from the :class:`~element.Element` class.
42 |
43 | """
44 |
45 |
46 |
--------------------------------------------------------------------------------
/quickly/dom/base.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Some general element types and some base classes for the quickly.dom elements.
23 | """
24 |
25 |
26 | ### NOTE: if adding classes/functions here, update docs/source/dom/base.rst!
27 |
28 |
29 | import re
30 |
31 | import parce.action as a
32 | from parce import lexicon, transform
33 |
34 | from . import element
35 |
36 |
37 | ## Base classes:
38 |
39 | class Document(element.Element):
40 | """Base class for a full source document."""
41 |
42 | space_between = '\n\n'
43 |
44 | def concat_space(self, n, m):
45 | if isinstance(n, (SinglelineComment, Newline)):
46 | return '\n'
47 | return self.space_between
48 |
49 |
50 | class String(element.TextElement):
51 | """Base class for a string element."""
52 |
53 | @classmethod
54 | def read_head(cls, origin):
55 | return ''.join(t.text[1:] if t.action is a.String.Escape else t.text
56 | for t in origin[1:-1])
57 |
58 | def write_head(self):
59 | return '"{}"'.format(re.sub(r'([\\"])', r'\\\1', self.head))
60 |
61 |
62 | class Comment(element.TextElement):
63 | """Base class for a comment element."""
64 |
65 |
66 | class SinglelineComment(Comment):
67 | """Base class for a multiline comment element."""
68 | def indent_override(self):
69 | """Returns 0 if this comment has at least three comment characters
70 | at the beginning.
71 |
72 | If it is on a line on its own, the current indent will then be ignored.
73 |
74 | """
75 | head = self.write_head()[:3]
76 | if len(head) == 3 and len(set(head)) == 1:
77 | return 0
78 |
79 |
80 | class MultilineComment(Comment):
81 | """Base class for a multiline comment element."""
82 |
83 |
84 | class BackslashCommand(element.TextElement):
85 | r"""A command that starts with a backslash, like in LaTeX and LilyPond.
86 |
87 | The backslash (``\``) is not in the head value.
88 |
89 | """
90 | @classmethod
91 | def check_head(cls, head):
92 | r"""Return False if the head value starts with a backslash (``\``)."""
93 | return not head.startswith('\\')
94 |
95 | @classmethod
96 | def read_head(cls, origin):
97 | """Strip the backslash of the origin token."""
98 | text = ''.join(t.text for t in origin)[1:]
99 | return text
100 |
101 | def write_head(self):
102 | """Add the backslash on write-out."""
103 | return '\\' + self.head
104 |
105 |
106 | ## Generic elements:
107 |
108 | class Newline(element.Element):
109 | """A Newline.
110 |
111 | Not created from existing documents, but you can insert this node
112 | anywhere you want a newline in manually crafted documents.
113 |
114 | """
115 | head = ''
116 | space_after = '\n'
117 |
118 |
119 | class BlankLine(element.Element):
120 | """A blank line.
121 |
122 | Not created from existing documents, but you can insert this node
123 | anywhere you want a blank line in manually crafted documents.
124 |
125 | """
126 | head = ''
127 | space_after = '\n\n'
128 |
129 |
130 | class Line(element.Element):
131 | """Container that prints the child nodes on one line with a space in between.
132 |
133 | Not created from existing documents, but you can insert this node in a
134 | Document when you want some nodes to be on the same line, for example when
135 | you want to write a comment at the end of the preceding line instead of on
136 | a line of its own.
137 |
138 | """
139 | space_before = space_after = '\n'
140 | space_between = ' '
141 |
142 |
143 | class Column(element.Element):
144 | """Container that prints every child node on a new line.
145 |
146 | Not created from existing documents, but you can insert this node in a
147 | Document when you want some nodes to be stacked vertically.
148 |
149 | """
150 | space_before = space_after = space_between = '\n'
151 |
152 |
153 | class Text(element.TextElement):
154 | """Generic text that is printed unmodified."""
155 |
156 |
157 | ## Special element:
158 |
159 | class Unknown(element.HeadElement):
160 | """Represents a document region that is not transformed.
161 |
162 | This element can only occur in documents transformed from source. It is
163 | used to denote reqions that are not transformed, such as CSS style tags or
164 | attributes, or script tags, in Html documents containing LilyPond music.
165 |
166 | *Parce* has fancy highlighting for those text fragments, but it makes no
167 | sense to try to transform those also to useful DOM nodes. So instead, we
168 | simply record the positions in the source document of these fragments using
169 | the first and the last token of such contexts that are not transformed.
170 |
171 | Calling :meth:`write_head` on this element results in an exception, because
172 | it does not know how it looks. But it knows the position in the document,
173 | because the first and the last untransformed tokens are in the origin.
174 |
175 | Before you can write out a document containing :class:`Unknown` elements
176 | fully out (e.g. using :meth:`~.element.Element.write` or
177 | :meth:`~.element.Element.write_indented`), you should replace those
178 | elements with e.g. :class:`Text` elements that have the text such as it
179 | appears in the source document. This can be done using
180 | :func:`.util.replace_unknown`.
181 |
182 | You *can* :meth:`~.element.Element.edit` documents with this element however,
183 | it will simply leave the unknown parts of the document as they are.
184 |
185 | """
186 | def write_head(self):
187 | """Raise RuntimeError."""
188 | raise RuntimeError(
189 | "can't write head value of Unknown element.\n"
190 | "Hint: replace Unknown with Text elements.")
191 |
192 |
193 | ## Language and Transform base/helper classes
194 |
195 | class XmlLike:
196 | """Mixin class for a language definition that bases on parce.lang.Xml.
197 |
198 | Adds the comsume attribute to some lexicons, like comment and string, which
199 | makes transforming easier.
200 |
201 | """
202 | @lexicon(consume=True)
203 | def comment(cls):
204 | yield from super().comment
205 |
206 | @lexicon(consume=True)
207 | def sqstring(cls):
208 | yield from super().sqstring
209 |
210 | @lexicon(consume=True)
211 | def dqstring(cls):
212 | yield from super().dqstring
213 |
214 | @lexicon(consume=True)
215 | def cdata(cls):
216 | yield from super().cdata
217 |
218 | @lexicon(consume=True)
219 | def processing_instruction(cls):
220 | yield from super().processing_instruction
221 |
222 |
223 | class Transform(transform.Transform):
224 | """Transform base class that keeps the origin tokens.
225 |
226 | Provides the :meth:`factory` method that creates the DOM node.
227 |
228 | """
229 | def factory(self, element_class, head_origin, tail_origin=(), *children):
230 | """Create an Element, keeping its origin.
231 |
232 | The ``head_origin`` and optionally ``tail_origin`` is an iterable of
233 | Token instances. All elements should be created using this method, so
234 | that it can be overridden for the case you don't want to remember the
235 | origin.
236 |
237 | """
238 | return element_class.with_origin(tuple(head_origin), tuple(tail_origin), *children)
239 |
240 |
241 | class AdHocTransform:
242 | """Transform mixin class that does *not* keep the origin tokens.
243 |
244 | This is used to create pieces (nodes) of a LilyPond document from text, and
245 | then use that pieces to compose a larger Document or to edit an existing
246 | document. It is undesirable that origin tokens then would mistakenly be
247 | used as if they originated from the document that's being edited.
248 |
249 | """
250 | def factory(self, element_class, head_origin, tail_origin=(), *children):
251 | """Create an Item *without* keeping its origin.
252 |
253 | The ``head_origin`` and optionally ``tail_origin`` is an iterable of
254 | Token instances. All items should be created using this method, so that
255 | it can be overridden for the case you don't want to remember the
256 | origin.
257 |
258 | """
259 | return element_class.from_origin(tuple(head_origin), tuple(tail_origin), *children)
260 |
261 |
262 |
--------------------------------------------------------------------------------
/quickly/dom/edit.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | The Edit base class, to perform operations on a DOM document and/or a related
23 | parce Document in different ways.
24 | """
25 |
26 | import parce.document
27 |
28 | from ..node import Range
29 | from .element import Element
30 |
31 |
32 | class Edit:
33 | """Base class to perform operations on a DOM document via a
34 | :class:`~quickly.node.Range`, an :class:`.element.Element` node, a
35 | :class:`parce.Document` or a selection of a parce document in a
36 | :class:`parce.Cursor`.
37 |
38 | You must implement at least :meth:`Edit.edit_range` to make it work. You
39 | can choose to reimplement other methods to alter behaviour or
40 | functionality.
41 |
42 | Then you create an instance, and you can call one of the ``edit_xxx()``
43 | methods.
44 |
45 | """
46 | _document = None # The parce document, if available
47 | _changes = 0 # The result of Element.edit(document)
48 |
49 | #: If True, when there is a selection, a Range is created from the root
50 | #: node, otherwise from the younghest common ancestor.
51 | range_from_root = False
52 |
53 | #: If True, a Range is created from the cursor's position to the end,
54 | #: instead of the full document in case there is no selection.
55 | range_from_cursor = False
56 |
57 | #: If True, does not write back changes to the parce Document
58 | readonly = False
59 |
60 | def document(self):
61 | """Return the parce Document, if available.
62 |
63 | This is the document that was used when :meth:`edit_cursor` or
64 | :meth:`edit_document` was called. The document is only available during
65 | that edit call.
66 |
67 | """
68 | return self._document
69 |
70 | def changes(self):
71 | """Return the number of changes made to the parce Document."""
72 | return self._changes
73 |
74 | def find_block(self, node):
75 | """The parce Block of the node (None if there is no parce Document)."""
76 | if node.pos is not None:
77 | doc = self.document()
78 | if doc:
79 | return doc.find_block(node.pos)
80 |
81 | def edit(self, music):
82 | """Convenience method calling one of the other edit_xxx methods depending on the type."""
83 | meth = (self.edit_cursor if isinstance(music, parce.Cursor)
84 | else self.edit_document if isinstance(music, parce.document.AbstractDocument)
85 | else self.edit_range if isinstance(music, Range)
86 | else self.edit_node if isinstance(music, Element)
87 | else None)
88 | if meth:
89 | return meth(music)
90 | raise TypeError('unknown music type')
91 |
92 | def edit_cursor(self, cursor):
93 | """Edit the range pointed to by the :class:`parce.Cursor`.
94 |
95 | The default implementation calls :meth:`edit_range` with a Range, that
96 | by default encompasses the full DOM tree when there is no selection.
97 | Set :attr:`range_from_cursor` to True if you want the edited range to
98 | be from the cursor's position to the document end when there is no
99 | selection.
100 |
101 | If the cursor has a selection, the Range encompasses only the child
102 | nodes that are within the cursor's selection. The ancestor of the range
103 | is by default the younghest common ancestor of the start and end nodes
104 | of the range. Set :attr:`range_from_root` to True if you want the
105 | ancestor of the range to be the DOM tree root anyway.
106 |
107 | """
108 | d = cursor.document().get_transform(True)
109 | r = start = end = None
110 | if cursor.has_selection():
111 | start_node = d.find_descendant_right(cursor.pos)
112 | end_node = d.find_descendant_left(cursor.end)
113 | if end_node or start_node:
114 | r = Range.from_nodes(start_node, end_node, self.range_from_root)
115 | if start_node:
116 | start = start_node.pos
117 | if end_node:
118 | end = end_node.end
119 | elif self.range_from_cursor:
120 | start_node = d.find_descendant_right(cursor.pos)
121 | if start_node:
122 | r = Range.from_nodes(start_node)
123 | if r is None:
124 | r = Range(d)
125 | self._document = cursor.document()
126 | self._changes = 0
127 | result = self.edit_range(r)
128 | if not self.readonly:
129 | self._changes = r.ancestor().edit(cursor.document(), start=start, end=end)
130 | self._document = None
131 | return result
132 |
133 | def edit_document(self, document):
134 | """Edit the full :class:`parce.Document`.
135 |
136 | The default implementation calls :meth:`edit_cursor` with a
137 | :class:`parce.Cursor` pointing to the beginning of the document,
138 | without selection.
139 |
140 | """
141 | return self.edit_cursor(parce.Cursor(document))
142 |
143 | def edit_node(self, node):
144 | """Edit the full :class:`.element.Element` node.
145 |
146 | The default implementation calls :meth:`edit_range` with a Range
147 | encompassing the full element node.
148 |
149 | """
150 | return self.edit_range(Range(node))
151 |
152 | def edit_range(self, r):
153 | """Edit the specified :class:`~quickly.node.Range`.
154 |
155 | At least this method needs to be implemented to actually perform the
156 | operation.
157 |
158 | """
159 | raise NotImplementedError
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/quickly/dom/indent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Functionality to pretty-print a DOM document, with good default indentation.
23 |
24 | """
25 |
26 | from . import util
27 |
28 |
29 | class _IndentLevel:
30 | """Holds the information for a new indent level."""
31 | def __init__(self, node):
32 | self.align_indices = tuple(node.indent_align_indices())
33 | self.align_positions = {}
34 |
35 | def align_child(self, child_index, pos):
36 | """Store a position value for the child at the specified index."""
37 | self.align_positions[child_index] = pos
38 |
39 | def get_align_pos(self):
40 | """Get a stored align value if the node supports it.
41 |
42 | Otherwise, None is returned.
43 |
44 | """
45 | for index in self.align_indices:
46 | try:
47 | return self.align_positions[index]
48 | except KeyError:
49 | pass
50 |
51 |
52 | class _Block:
53 | """Keeps the administration of a line of output text.
54 |
55 | The number of spaces to indent this line is in the ``indent`` attribute;
56 | the line itself is built as a list in the ``line`` attribute.
57 |
58 | """
59 | def __init__(self, indent):
60 | self.indent = indent
61 | self.line = []
62 |
63 | def offset(self, pos):
64 | """Get the total length of the text in the first ``pos`` pieces."""
65 | return sum(map(len, self.line[:pos]))
66 |
67 | def output(self):
68 | """Get the output line."""
69 | return ' ' * self.indent + ''.join(self.line) + '\n'
70 |
71 |
72 | class Indenter:
73 | """Prints the indented output of a node.
74 |
75 | Indentation preferences can be given on instantiation or by setting the
76 | attributes of the same name.
77 |
78 | The default ``indent_width`` can be given, and the additional
79 | ``start_indent`` which is prepended to every output line, both in number of
80 | spaces and defaulting to 0.
81 |
82 | The ``max_align_indent`` argument determines the number of spaces used
83 | at most to align indenting lines with text on previous lines. If the
84 | number is exceeded, the default ``indent_width`` is used instead on
85 | such lines.
86 |
87 | Call :meth:`write` to get the indented text output of a node.
88 |
89 | """
90 | def __init__(self,
91 | indent_width = 2,
92 | start_indent = 0,
93 | max_align_indent = 16,
94 | ):
95 |
96 | #: the default indent width
97 | self.indent_width = indent_width
98 |
99 | #: the number of spaces to prepend to every output line
100 | self.start_indent = start_indent
101 |
102 | #: the maximum number of spaces to indent to align a line with certain text on the previous line
103 | self.max_align_indent = max_align_indent
104 |
105 | # initialize working variables
106 | self._output = [] # the list in which the result output is built up
107 | self._indent_stack = [] # the list of indenting history
108 | self._indenters = [] # the list of indenters created in the current line
109 | self._dedenters = 0 # the negative count of indent levels to end
110 | self._whitespace = [] # collects the minimal amount of whitespace between nodes
111 | self._can_dedent = False # can the current line be dedented
112 |
113 | def write(self, node):
114 | """Get the indented output of the node.
115 |
116 | Called by :meth:`Element.write_indented() `.
117 |
118 | """
119 | self._output.clear()
120 | self._indent_stack.clear()
121 | self._indenters.clear()
122 | self._dedenters = 0
123 | self._whitespace.clear()
124 | self.create_new_block()
125 | self.output_node(node)
126 |
127 | # strip preceding space
128 | result = self._output
129 | while result:
130 | if result[0].line:
131 | if result[0].line[0].isspace():
132 | del result[0].line[0]
133 | else:
134 | break
135 | else:
136 | del result[0]
137 |
138 | return ''.join(block.output() for block in result)
139 |
140 | def output_node(self, node, index=-1):
141 | """*(Internal.)* Output one node and its children.
142 |
143 | The index, if given, is the index of this node in its parent. This is
144 | used to get additional indenting hints for the node.
145 |
146 | """
147 | head = node.write_head()
148 | tail = node.write_tail()
149 | has_children = len(node) > 0
150 | indent = node.indent_children()
151 |
152 | self.add_whitespace(node.space_before)
153 |
154 | if head:
155 | self.output_head(head, index, node.indent_override())
156 | if tail or has_children:
157 | self.add_whitespace(node.space_after_head)
158 |
159 | if indent:
160 | self.enter_indent(node)
161 |
162 | if has_children:
163 | n = node[0]
164 | self.output_node(n, 0)
165 | for i, m in enumerate(node[1:], 1):
166 | self.add_whitespace(node.concat_space(n, m))
167 | self.output_node(m, i)
168 | n = m
169 |
170 | if indent:
171 | self.leave_indent()
172 |
173 | if tail:
174 | self.add_whitespace(node.space_before_tail)
175 | self.output_tail(tail)
176 |
177 | self.add_whitespace(node.space_after)
178 |
179 | def add_whitespace(self, whitespace):
180 | """*(Internal.)* Add whitespace, which is combined as soon as text is printed out."""
181 | self._whitespace.append(whitespace)
182 |
183 | def enter_indent(self, node):
184 | """*(Internal.)* Enter a new indent level for the ``node``."""
185 | self._indenters.append(_IndentLevel(node))
186 |
187 | def leave_indent(self):
188 | """*(Internal.)* Leave the younghest indent level."""
189 | if self._indenters:
190 | self._indenters.pop()
191 | else:
192 | self._dedenters -= 1
193 |
194 | def current_indent(self):
195 | """*(Internal.)* Get the current indent (including ``start_indent``)
196 | in nr of spaces.
197 |
198 | """
199 | return self._indent_stack[-1] if self._indent_stack else self.start_indent
200 |
201 | def create_new_block(self):
202 | """*(Internal.)* Go to a new line."""
203 | if self._dedenters:
204 | # remove some levels
205 | del self._indent_stack[self._dedenters:]
206 | self._dedenters = 0
207 |
208 | if self._indenters:
209 | # add new indent levels
210 | new_indent = current_indent = self.current_indent()
211 | for i in self._indenters:
212 | new_indent += self.indent_width
213 | pos = i.get_align_pos()
214 | if pos is not None:
215 | align_indent = self._output[-1].offset(pos)
216 | if align_indent <= self.max_align_indent:
217 | new_indent = current_indent + align_indent
218 | self._indent_stack.append(new_indent)
219 | self._indenters.clear()
220 |
221 | current_indent = self.current_indent()
222 |
223 | self._can_dedent = bool(self._indent_stack)
224 | self._output.append(_Block(current_indent))
225 |
226 | def output_space(self):
227 | """*(Internal.)* Output whitespace. Newlines start a new output line."""
228 | for c in util.collapse_whitespace(self._whitespace):
229 | if c == '\n':
230 | self.create_new_block()
231 | else:
232 | self._output[-1].line.append(c)
233 | self._whitespace.clear()
234 |
235 | def output_head(self, text, index=-1, override=None):
236 | """*(Internal.)* Output head text.
237 |
238 | The ``index``, if given, is the index of the node in its parent. This
239 | is used to get additional indenting hints for the node.
240 |
241 | If ``override`` is not None, and this head text happens to be the first
242 | on a new line, this value is used as the indent depth for this line,
243 | in stead of the current indent.
244 |
245 | """
246 | self.output_space()
247 | self._can_dedent = False
248 | last_line = self._output[-1].line
249 | if not last_line and override is not None:
250 | self._output[-1].indent = override
251 | if self._indenters and index in self._indenters[-1].align_indices:
252 | # store the position of the node on the current output line
253 | position = len(last_line)
254 | self._indenters[-1].align_child(index, position)
255 | last_line.append(text)
256 |
257 | def output_tail(self, text):
258 | """*(Internal.)* Output the tail text."""
259 | self.output_space()
260 | if self._can_dedent and self._dedenters:
261 | del self._indent_stack[self._dedenters]
262 | self._dedenters = 0
263 | self._output[-1].indent = self.current_indent()
264 | self._output[-1].line.append(text)
265 |
266 |
267 |
--------------------------------------------------------------------------------
/quickly/dom/markup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2021 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Helper module to manually construct markup elements.
23 |
24 | Usage example::
25 |
26 | >>> import quickly.dom.markup as m
27 | >>> mkup = m.markup(m.bold("text", "text2"))
28 | >>> mkup.write()
29 | '\\markup \\bold { text text2 }'
30 | >>> mkup.dump()
31 |
32 | ╰╴
33 | ╰╴
34 | ├╴
35 | ╰╴
36 |
37 | """
38 |
39 | import functools
40 | import keyword
41 | import re
42 |
43 | import parce.lang.lilypond
44 | import parce.lang.lilypond_words as w
45 |
46 | from . import base, element, lily, scm
47 |
48 |
49 | # helpers
50 | _c = lily.MarkupCommand
51 | _s = lambda n: lily.Scheme('#', n)
52 | _a = lambda v: lily.Scheme('#', scm.create_element_from_value(v))
53 | _q = lambda n: lily.Scheme('#', scm.Quote("'", n))
54 | _sym = lambda s: lily.Scheme('#', scm.Quote("'", scm.Identifier(s)))
55 | _pair = lambda x, y: lily.Scheme('#', scm.Quote("'", scm.p(x, y)))
56 |
57 |
58 | _RE_MATCH_MARKUP = re.compile(parce.lang.lilypond.RE_LILYPOND_MARKUP_TEXT).fullmatch
59 |
60 |
61 | def is_markup(text):
62 | """Return True if the text can be written as LilyPond markup without quotes."""
63 | return bool(_RE_MATCH_MARKUP(text))
64 |
65 |
66 | def _create_list(args):
67 | """Create a MarkupList when number of arguments is not 1.
68 |
69 | Also calls :func:`_auto_arg` on every argument.
70 |
71 | """
72 | if len(args) == 1:
73 | return _auto_arg(args[0])
74 | return lily.MarkupList(*map(_auto_arg, args))
75 |
76 |
77 | def _auto_arg(arg):
78 | """Create MarkupWord or Scheme if not already an Element.
79 |
80 | If arg is an element, it is returned unchanged. If arg is a :class:`str`, a
81 | :class:`~lily.MarkupWord` is created (or a :class:`~lily.String`, if there
82 | are non-printable characters). Otherwise, a Scheme expression is created of
83 | the corresponding type.
84 |
85 | """
86 | if isinstance(arg, element.Element):
87 | return arg
88 | elif isinstance(arg, str):
89 | return lily.MarkupWord(arg) if is_markup(arg) else lily.String(arg)
90 | return lily.Scheme('#', scm.create_element_from_value(arg))
91 |
92 |
93 | def markup(*args):
94 | r"""Return `\markup`; automatically wraps arguments in brackets."""
95 | return lily.Markup(r'markup', _create_list(args))
96 |
97 |
98 | def markuplist(*args):
99 | r"""Return `\markuplist`; automatically wraps arguments in brackets."""
100 | return lily.Markup(r'markuplist', lily.MarkupList(*map(_auto_arg, args)))
101 |
102 |
103 | ### markup commands with special agument handling
104 |
105 | def char(n):
106 | return _c('char', _s(scm.Hex(n)))
107 |
108 |
109 | def tied_lyric(text):
110 | return _c('tied-lyric', _s(scm.String(text)))
111 |
112 |
113 | def fret_diagram(s):
114 | return c_('fret-diagram', _s(scm.String(s)))
115 |
116 |
117 | def fret_diagram_terse(s):
118 | return c_('fret-diagram-terse', _s(scm.String(s)))
119 |
120 |
121 | def fret_diagram_verbose(element):
122 | return c_('fret-diagram-verbose', element)
123 |
124 |
125 | def fromproperty(name):
126 | return _c('fromproperty', _sym(name))
127 |
128 |
129 | def harp_pedal(s):
130 | return _c('harp-pedal', lily.String(s))
131 |
132 |
133 | def justify_field(name):
134 | return _c('justify-field', _sym(name))
135 |
136 |
137 | def justify_string(s):
138 | return _c('justify-string', _s(scm.String(s)))
139 |
140 |
141 | def lookup(s):
142 | return _c('lookup', _s(scm.String(s)))
143 |
144 |
145 | def musicglyph(name):
146 | return _c('musicglyph', _s(scm.String(name)))
147 |
148 |
149 | def postscript(s):
150 | if isinstance(s, str):
151 | s = _s(scm.String(s))
152 | return _c('postscript', s)
153 |
154 |
155 | def rest(duration):
156 | r"""The ``\rest`` markup command.
157 |
158 | The ``duration`` can be a markup object containing a word that is a
159 | duration, e.g. ``4..`` (for LilyPond >= 2.22) or a Scheme string like
160 | ``#"4.."`` (for LilyPond < 2.22).
161 |
162 | """
163 | if isinstance(duration, str):
164 | duration = lily.MarkupList(lily.MarkupWord(duration))
165 | return _c('rest', duration)
166 |
167 |
168 | def score(*elements):
169 | r"""The ``\score`` markup command.
170 |
171 | You may give Header, Layout and general Music nodes.
172 |
173 | """
174 | return lily.MarkupScore(*elements)
175 |
176 |
177 | def score_lines(*elements):
178 | r"""The ``\score-lines`` markup command.
179 |
180 | You may give Header, Layout and general Music nodes.
181 |
182 | """
183 | return lily.MarkupScoreLines(*elements)
184 |
185 |
186 | def verbatim_file(filename):
187 | return _c('verbatim-file', _s(scm.String(filename)))
188 |
189 |
190 | def wordwrap_field(name):
191 | return _c('wordwrap-field', _sym(name))
192 |
193 |
194 | def wordwrap_string(s):
195 | return _c('wordwrap-string', _s(scm.String(s)))
196 |
197 |
198 | def note(duration, direction):
199 | r"""The ``\note`` markup command.
200 |
201 | The ``duration`` can be a markup object containing a word that is a
202 | duration, e.g. ``4..`` (for LilyPond >= 2.22) or a Scheme string like
203 | ``#"4.."`` (for LilyPond < 2.22).
204 |
205 | The ``direction`` is a floating point value; the sign is the stem
206 | direction, the value the stem length.
207 |
208 | """
209 | if isinstance(duration, str):
210 | duration = lily.MarkupList(lily.MarkupWord(duration))
211 | return _c('note', duration, _s(scm.Number(direction)))
212 |
213 |
214 | def override(prop, value, *args):
215 | r"""The ``\override`` markup command.
216 |
217 | The ``prop`` should be a string, the ``value`` a Scheme value (Python bool,
218 | int or float are handled automatically).
219 |
220 | """
221 | value = scm.create_element_from_value(value)
222 | return _c('override', _pair(scm.Identifier(prop), value), _create_list(args))
223 |
224 |
225 | def override_lines(prop, value, *args):
226 | r"""The ``\override-lines`` command.
227 |
228 | The ``prop`` should be a string, the ``value`` a Scheme value (Python bool,
229 | int or float are handled automatically).
230 |
231 | """
232 | value = scm.create_element_from_value(value)
233 | return _c('override-lines', _pair(scm.Identifier(prop), value), _create_list(args))
234 |
235 |
236 | def translate(x, y, *args):
237 | return _c('translate', _pair(x, y), _create_list(args))
238 |
239 |
240 | def translate_scaled(x, y, *args):
241 | return _c('translate-scaled', _pair(x, y), _create_list(args))
242 |
243 |
244 | def with_link(label, *args):
245 | return _c('with-color', _sym(label), _create_list(args))
246 |
247 |
248 | def with_url(url, *args):
249 | return _c('with-url', _s(scm.String(url)), _create_list(args))
250 |
251 |
252 | def woodwind_diagram(instrument, scheme_commands):
253 | return _c('woodwind-diagram', _sym(label), scheme_commands)
254 |
255 |
256 | def draw_squiggle_line(sqlength, x, y, eqend):
257 | return _c('draw-squiggle-line', _a(sqlength), _pair(x, y), _a(eqend))
258 |
259 |
260 | def epsfile(axis, size, filename):
261 | return _c('epsfile', _a(axis), _a(size), _a(filename))
262 |
263 |
264 | def filled_box(x1, y1, x2, y2, blot):
265 | return _c('filled-box', _pair(x1, x2), _pair(y1, y2), _a(blot))
266 |
267 |
268 | def note_by_number(log, dotcount, direction):
269 | return _c('note-by-number', _s(scm.Number(log)), _s(scm.Number(dotcount)), _a(direction))
270 |
271 |
272 | def pad_to_box(x1, y1, x2, y2, *args):
273 | return _c('pad-to-box', _pair(x1, x2), _pair(y1, y2), _create_list(args))
274 |
275 |
276 | def page_ref(label, gauge, *mkup):
277 | return _c('page-ref', _sym(label), _auto_arg(gauge), _create_list(mkup))
278 |
279 |
280 | def with_dimensions(x1, y1, x2, y2, *args):
281 | return _c('with-dimensions', _pair(x1, x2), _pair(y1, y2), _create_list(args))
282 |
283 |
284 | def fill_with_pattern(space, direction, pattern, left, right):
285 | return _c('fill-with-pattern',
286 | _a(space), _a(direction),
287 | _auto_arg(pattern), _auto_arg(left), _auto_arg(right))
288 |
289 |
290 |
291 |
292 |
293 | def _main():
294 | """Auto-create markup factory functions."""
295 | factories = (
296 | (lambda n: lambda: _c(n)),
297 | (lambda n: lambda *text: _c(n, _create_list(text))),
298 | (lambda n: lambda arg, *text: _c(n, _auto_arg(arg), _create_list(text))),
299 | (lambda n: lambda arg1, arg2, *text: _c(n, *map(_auto_arg, (arg1, arg2)), _create_list(text))),
300 | (lambda n: lambda arg1, arg2, arg3, *text: _c(n, *map(_auto_arg, (arg1, arg2, arg3)), _create_list(text))),
301 | None,
302 | )
303 |
304 | for argcount, factory in enumerate(factories):
305 | for cmd in w.markup_commands_nargs[argcount]:
306 | name = cmd.replace('-', '_')
307 | if keyword.iskeyword(name):
308 | name += '_'
309 | doc = r"The ``\{}`` markup command.".format(cmd)
310 | try:
311 | f = globals()[name]
312 | except KeyError:
313 | if factory:
314 | func = factory(cmd)
315 | func.__name__ = name
316 | func.__qualname__ = name
317 | func.__doc__ = doc
318 | globals()[name] = func
319 | else:
320 | if not f.__doc__:
321 | f.__doc__ = doc
322 |
323 |
324 |
325 | _main()
326 | del _main
327 |
--------------------------------------------------------------------------------
/quickly/dom/read.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2021 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Simple helper functions to easily build DOM elements reading from text.
23 |
24 | By default the generated DOM nodes do not know their position in the
25 | originating text, because the origin tokens are not preserved. This is the best
26 | when building DOM snippets using this module and inserting them in existing
27 | documents.
28 |
29 | If you set the ``with_origin`` argument in the reader functions to True, the
30 | origin tokens are preserved, so the DOM nodes know their position in the
31 | originating text. Do not insert these nodes in a DOM document originating from
32 | another text source if you want to edit that text via the DOM document later,
33 | because the positions of the nodes can't be trusted then, and that may lead to
34 | errors. (Of course this is no problem when you are writing a document from
35 | scratch.)
36 |
37 | """
38 |
39 |
40 | from parce.transform import Transformer
41 |
42 | from ..lang import (
43 | # docbook,
44 | latex,
45 | lilypond,
46 | html,
47 | scheme,
48 | # texinfo,
49 | )
50 |
51 |
52 | # init two transformers, accessible by 0 (False) and 1 (True) :-)
53 | _transformer = [Transformer(), Transformer()]
54 | _transformer[0].transform_name_template = "{}AdHocTransform"
55 |
56 |
57 | def htm_document(text, with_origin=False):
58 | """Return a :class:`.htm.Document` from the text.
59 |
60 | Example::
61 |
62 | >>> from quickly.dom import read
63 | >>> node = read.htm_document('Title Text...
')
64 | >>> node.dump()
65 |
66 | ╰╴
67 | ├╴
68 | │ ╰╴
69 | ├╴
70 | │ ├╴
71 | │ │ ╰╴
72 | │ ├╴
73 | │ ╰╴
74 | │ ╰╴
75 | ├╴
76 | │ ├╴
77 | │ │ ╰╴
78 | │ ├╴
79 | │ ╰╴
80 | │ ╰╴
81 | ╰╴
82 | ╰╴
83 | >>> node.write()
84 | 'Title Text...
'
85 |
86 | If you want the generated nodes to know the position in the original text,
87 | you should keep the origin tokens and set ``with_origin`` to True:
88 |
89 | >>> node = read.htm_document('Title Text...
', True)
90 | >>> node.dump()
91 |
92 | ╰╴
93 | ├╴
94 | │ ╰╴
95 | ├╴
96 | │ ├╴
97 | │ │ ╰╴
98 | │ ├╴
99 | │ ╰╴
100 | │ ╰╴
101 | ├╴
102 | │ ├╴
103 | │ │ ╰╴
104 | │ ├╴
105 | │ ╰╴
106 | │ ╰╴
107 | ╰╴
108 | ╰╴
109 |
110 | """
111 | return _transformer[with_origin].transform_text(html.Html.root, text)
112 |
113 |
114 | def htm(text, with_origin=False):
115 | """Return one element from the text, read in Html.root."""
116 | for node in htm_document(text, with_origin):
117 | return node
118 |
119 |
120 | def lily_document(text, with_origin=False):
121 | """Return a :class:`.lily.Document` from the text.
122 |
123 | Example::
124 |
125 | >>> from quickly.dom import read
126 | >>> node = read.lily_document("music = { c d e f g }")
127 | >>> node.write()
128 | 'music = { c d e f g }'
129 | >>> node.dump()
130 |
131 | ╰╴
132 | ├╴
133 | │ ╰╴
134 | ├╴
135 | ╰╴
136 | ├╴
137 | ├╴
138 | ├╴
139 | ├╴
140 | ╰╴
141 |
142 | This way you can get a DOM document from a full source text. This can also
143 | help you to create DOM element nodes that would otherwise be tedious to
144 | construct and type.
145 |
146 | """
147 | return _transformer[with_origin].transform_text(lilypond.LilyPond.root, text)
148 |
149 |
150 | def lily(text, with_origin=False):
151 | """Return one element from the text, read in LilyPond.root.
152 |
153 | Examples::
154 |
155 | >>> from quickly.dom import read
156 | >>> read.lily("a")
157 |
158 |
159 | >>> color = read.lily("#(x11-color 'DarkSlateBlue4)")
160 | >>> color.dump()
161 |
162 | ╰╴
163 | ├╴
164 | ╰╴
165 | ╰╴
166 |
167 | >>> read.lily("##f").dump()
168 |
169 | ╰╴
170 |
171 | >>> read.lily("1/2")
172 |
173 |
174 | >>> read.lily("##xa/b").dump()
175 |
176 | ╰╴
177 |
178 | This can help you to create DOM element nodes that would otherwise be
179 | tedious to construct and type.
180 |
181 | """
182 | for node in lily_document(text, with_origin):
183 | return node
184 |
185 |
186 | def scm_document(text, with_origin=False):
187 | """Return a :class:`.scm.Document` from the text."""
188 | return _transformer[with_origin].transform_text(scheme.Scheme.root, text)
189 |
190 |
191 | def scm(text, with_origin=False):
192 | """Return one element from the text, read in Scheme.root."""
193 | for node in scm_document(text, with_origin):
194 | return node
195 |
196 |
197 | def tex_document(text, with_origin=False):
198 | """Return a :class:`.tex.Document` from the text."""
199 | return _transformer[with_origin].transform_text(latex.Latex.root, text)
200 |
201 |
202 | def tex(text, with_origin=False):
203 | """Return one :mod:`.tex` node from the text."""
204 | for node in tex_document(text, with_origin):
205 | return node
206 |
207 |
208 |
--------------------------------------------------------------------------------
/quickly/dom/scm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Elements needed for Scheme expressions.
23 |
24 | Besides the elements a few functions are provided to make it easier to
25 | manually construct scheme expressions. For example::
26 |
27 | >>> from quickly.dom.scm import q, qq, uq, i, p, s
28 | >>> s(True)
29 |
30 | >>> s(100)
31 |
32 | >>> s(100.123)
33 |
34 | >>> s(('text', -2)).dump()
35 |
36 | ├╴
37 | ├╴
38 | ╰╴
39 | >>> s(('text', -2)).write()
40 | '("text" . -2)'
41 | >>> q(s((i('text'), -2))).write()
42 | "'(text . -2)"
43 | >>> n = s([i('if'), [i('<'), i('a'), 100], "smaller", "larger"])
44 | >>> n.dump()
45 |
46 | ├╴
47 | ├╴
48 | │ ├╴
49 | │ ├╴
50 | │ ╰╴
51 | ├╴
52 | ╰╴
53 | >>> n.write()
54 | '(if (< a 100) "smaller" "larger")'
55 |
56 | """
57 |
58 | import fractions
59 | import math
60 |
61 | from parce.lang.scheme import scheme_is_indenting_keyword
62 |
63 | from . import base, element
64 |
65 |
66 | class LilyPond(element.BlockElement):
67 | """A LilyPond block inside Scheme, between ``#{`` and ``#}``."""
68 | head = "#{"
69 | tail = "#}"
70 |
71 |
72 | class Document(base.Document):
73 | """A full Scheme document."""
74 |
75 |
76 | class SinglelineComment(base.SinglelineComment):
77 | """A singleline comment in Scheme after ``;``."""
78 | space_after = '\n'
79 |
80 | @classmethod
81 | def read_head(cls, origin):
82 | return ''.join(t.text for t in origin[1:])
83 |
84 | def write_head(self):
85 | return ';{}'.format(self.head)
86 |
87 |
88 | class MultilineComment(base.MultilineComment):
89 | """A multiline comment in Scheme after ``#!``."""
90 | @classmethod
91 | def read_head(cls, origin):
92 | end = -1 if origin[-1] == "#!" else None
93 | return ''.join(t.text for t in origin[1:end])
94 |
95 | def write_head(self):
96 | return '#!{}#!'.format(self.head)
97 |
98 |
99 | class Char(element.TextElement):
100 | """A Scheme character."""
101 | @classmethod
102 | def read_head(cls, origin):
103 | return origin[0].text[2:] # leave out the '#\' prefix
104 |
105 | def write_head(self):
106 | return r'#\{}'.format(self.head)
107 |
108 |
109 | class String(base.String):
110 | """A quoted string."""
111 |
112 |
113 | class Identifier(element.TextElement):
114 | """A Scheme identifier (keyword, variable, symbol)."""
115 |
116 |
117 | class List(element.BlockElement):
118 | """A Scheme pair or list ( ... )."""
119 | space_between = " "
120 | head = "("
121 | tail = ")"
122 |
123 | def indent_align_indices(self):
124 | """How to align child nodes if on a new line."""
125 | for n in self:
126 | if isinstance(n, Identifier):
127 | if scheme_is_indenting_keyword(n.head):
128 | return
129 | yield 1 # prefer align at the second item
130 | break
131 | yield 0
132 |
133 |
134 | class Vector(element.BlockElement):
135 | """A Scheme vector #( ... )."""
136 | space_between = " "
137 | head = "#("
138 | tail = ")"
139 |
140 |
141 | class Quote(element.TextElement):
142 | r"""A Scheme quote ``'``, ``\```, ``,`` or ``,@``."""
143 | @classmethod
144 | def check_head(cls, head):
145 | return head in ("'", "`", ",", ",@")
146 |
147 |
148 | class Number(element.TextElement):
149 | """A decimal numerical value, and the base class for Hex, Bin, Oct.
150 |
151 | All features of Scheme numerical values are supported: exact/inexactness,
152 | polar coordinates, complex numbers, fractions, infinity, nan and unknown
153 | digits (#).
154 |
155 | """
156 | radix = 10
157 | _prefix = {2: "#b", 8: "#o", 10: "", 16: "#x" }
158 | _fmt = {2: "b", 8: "o", 10: "d", 16: "x" }
159 | @classmethod
160 | def read_head(cls, origin):
161 | from parce.lang.scheme import scheme_number
162 | return scheme_number(origin)
163 |
164 | def write_head(self):
165 | v = self.head
166 | if v == math.inf:
167 | s = '+inf.0'
168 | elif v == -math.inf:
169 | s = '-inf.0'
170 | elif v is math.nan:
171 | s = '+nan.0'
172 | elif isinstance(v, fractions.Fraction):
173 | fmt = self._fmt[self.radix]
174 | f = lambda n: format(n, fmt)
175 | s = f(v.numerator)
176 | if v.denominator != 1:
177 | s = '{}/{}'.format(s, f(v.denominator))
178 | elif isinstance(v, float) and self.radix == 10:
179 | s = str(v)
180 | elif isinstance(v, complex):
181 | s = '{}{:+}i'.format(v.real, v.imag)
182 | else:
183 | s = format(int(v), self._fmt[self.radix])
184 | return self._prefix[self.radix] + s
185 |
186 | def repr_head(self):
187 | """Dump Scheme numbers in Scheme syntax."""
188 | return self.write_head()
189 |
190 |
191 | class Bin(Number):
192 | """A Scheme binary integer value."""
193 | radix = 2
194 |
195 |
196 | class Oct(Number):
197 | """A Scheme octal integer value."""
198 | radix = 8
199 |
200 |
201 | class Hex(Number):
202 | """A Scheme hexadecimal integer value."""
203 | radix = 16
204 |
205 |
206 | class Bool(Number):
207 | """A Scheme boolean value."""
208 | @classmethod
209 | def read_head(cls, origin):
210 | return origin[0].text[1] in 'tT'
211 |
212 | def write_head(self):
213 | return '#t' if self.head else '#f'
214 |
215 |
216 | class NaN(Number):
217 | """Not a Number, created when a ``number`` context has invalid tokens."""
218 | @classmethod
219 | def read_head(cls, origin):
220 | return math.nan
221 |
222 |
223 | class Dot(element.HeadElement):
224 | """A dot, e.g. in a Scheme pair."""
225 | head = '.'
226 |
227 |
228 | def create_element_from_value(value):
229 | """Convert a regular Python value to a scheme Element node.
230 |
231 | Python bool, int, float or str values are converted into Bool, Number, or
232 | String objects respectively. A list is converted into a List element, and a
233 | tuple (of length > 1) in a pair, with a dot inserted before the last node.
234 | Element objects are returned unchanged.
235 |
236 | A KeyError is raised when there is no conversion for the value's type.
237 |
238 | """
239 | if isinstance(value, element.Element):
240 | return value
241 | return _element_mapping[type(value)](value)
242 |
243 |
244 | def q(arg):
245 | """Quote arg. Automatically converts arguments if needed."""
246 | return Quote("'", create_element_from_value(arg))
247 |
248 |
249 | def qq(arg):
250 | """Quasi-quote arg. Automatically converts arguments if needed."""
251 | return Quote("`", create_element_from_value(arg))
252 |
253 |
254 | def uq(arg):
255 | """Un-quote arg. Automatically converts arguments if needed."""
256 | return Quote(",", create_element_from_value(arg))
257 |
258 |
259 | def i(arg):
260 | """Make an identifier of str arg."""
261 | return Identifier(arg)
262 |
263 |
264 | def p(arg1, arg2, *args):
265 | """Return a pair (two-or-more element List with dot before last element)."""
266 | return create_element_from_value((arg1, arg2, *args))
267 |
268 |
269 | def s(arg):
270 | """Same as :func:`create_element_from_value`."""
271 | return create_element_from_value(arg)
272 |
273 |
274 | # used in the create_element_from_value function
275 | _element_mapping = {
276 | bool: Bool,
277 | int: Number,
278 | float: Number,
279 | fractions.Fraction: Number,
280 | str: String,
281 | list: (lambda value: List(*map(s, value))),
282 | tuple: (lambda value: List(*map(s, value[:-1]), Dot(), *map(s, value[-1:]))),
283 | }
284 |
285 |
--------------------------------------------------------------------------------
/quickly/dom/scope.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | The Scope class finds included documents.
23 | """
24 |
25 |
26 | import reprlib
27 | from urllib.parse import urljoin, urlparse
28 |
29 | import parce.util
30 |
31 | import quickly
32 |
33 |
34 | class Scope:
35 | """A Scope helps finding files included by a parce Document.
36 |
37 | Initialize Scope with a parce Document. The
38 | :attr:`~parce.document.AbstractDocument.url` attribute of the document
39 | helps finding included files. That url should be absolute.
40 |
41 | The ``parent`` is specified when a new Scope is created by
42 | :meth:`include_scope`.
43 |
44 | The ``factory`` keyword parameter and attribute is a callable that should
45 | return a :class:`parce.Document` for a filename. If you don't specify a
46 | factory, a default one is used that loads a Document from the file system,
47 | and if found, caches it using a cache based on the file's mtime.
48 |
49 | Specify a factory to use other caching, deferred loading, or to look for a
50 | document in a list of open documents in a GUI editor etc.
51 |
52 | If desired, add absolute urls to the :attr:`include_path` instance
53 | attribute, indicating folders where to search for includeable files.
54 |
55 | """
56 | def __init__(self, doc, parent=None, factory=None, node=None):
57 | if not factory:
58 | factory = parce.util.file_cache(quickly.load).__getitem__
59 | self._document = doc
60 | self.parent = parent #: The parent Scope (None for the root Scope)
61 | self.factory = factory #: A callable returning a :class:`parce.Document` for a filename.
62 | self.node = node #: The node that was specified to :meth:`include_scope`.
63 | #: A list of directories to search for \include-d files.
64 | self.include_path = parent.include_path if parent else []
65 | #: Whether to search in the directory of an included file for new includes.
66 | self.relative_include = parent.relative_include if parent else True
67 |
68 | def __repr__(self):
69 | return "<{} {}>".format(type(self).__name__, reprlib.repr(self.document().url))
70 |
71 | def document(self):
72 | """Return our parce Document."""
73 | return self._document
74 |
75 | def include_scope(self, url, node=None):
76 | """Return a child scope for the url.
77 |
78 | If the ``url`` is relative, it is resolved against our document's url
79 | (if :attr:`relative_include` is True), the root scope's url and the
80 | urls in the :attr:`include_path`.
81 |
82 | A ``node`` can be given, that's simply put in the :attr:`node`
83 | attribute of the returned child scope. It can be used to look further
84 | in the document that included the current document, to find e.g. a
85 | variable definition.
86 |
87 | Returns None if no includable document could be found. This scope
88 | inherits the factory, the include_path and the relative_include setting
89 | of ourselves.
90 |
91 | """
92 | for u in self.urls(url):
93 | doc = self.get_document(u)
94 | if doc:
95 | return type(self)(doc, self, self.factory, node)
96 |
97 | def ancestors(self):
98 | """Yield the ancestor scopes."""
99 | scope = self
100 | while scope.parent:
101 | scope = scope.parent
102 | yield scope
103 |
104 | def root(self):
105 | """The root scope."""
106 | scope = self
107 | for scope in self.ancestors():
108 | pass
109 | return scope
110 |
111 | def urls(self, url):
112 | """Return a list of unique urls representing possibly includable files.
113 |
114 | The list results from the filename of our document (if set and if
115 | :attr:`relative_include` is True), the filename of the document that
116 | started the include chain, and the include path.
117 |
118 | The urls are not checked for existence.
119 |
120 | """
121 | # skip urls already in parent scopes, prevents circular include hangs
122 | skip = {self.document().url}
123 | skip.update(scope.document().url for scope in self.ancestors())
124 |
125 | urls = []
126 | def add(base_url):
127 | if base_url:
128 | u = urljoin(base_url, url)
129 | if u not in skip and u not in urls:
130 | urls.append(u)
131 | if self.relative_include:
132 | add(self.document().url)
133 | add(self.root().document().url)
134 | for u in self.include_path:
135 | if not u.endswith('/'):
136 | u += '/'
137 | add(u)
138 | return urls
139 |
140 | def get_document(self, url):
141 | """Return a parce Document at url.
142 |
143 | Returns None if no document can be found. The default implementation
144 | calls :attr:`factory` with a filename pointing to the local file
145 | system. OSErrors raised by the factory are suppressed.
146 |
147 | """
148 | filename = urlparse(url).path
149 | try:
150 | return self.factory(filename)
151 | except OSError:
152 | pass
153 |
154 |
--------------------------------------------------------------------------------
/quickly/dom/tex.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Elements needed for Latex documents.
23 |
24 | .. note::
25 |
26 | This module does not create DOM nodes for all Latex content.
27 |
28 | You can build a full Latex document from scratch using the available nodes,
29 | but parce supports much more syntax than makes sense to build DOM nodes for.
30 |
31 | LilyPond music is always in a lily.Document nodes, and can occur in a braced
32 | command or an environment.
33 |
34 | """
35 |
36 | from . import base, element
37 |
38 |
39 | class Document(base.Document):
40 | """A full LaTeX source document."""
41 | space_between = ''
42 |
43 |
44 | class Option(element.BlockElement):
45 | """An option block: ``[`` ... ``]``."""
46 | head = '['
47 | tail = ']'
48 |
49 |
50 | class Brace(element.BlockElement):
51 | """A braced expression: ``{`` ... ``}``."""
52 | head = '{'
53 | tail = '}'
54 |
55 |
56 | class Math(element.BlockElement):
57 | """Abstract Math block. Subclasses define the start/end delimiters."""
58 |
59 |
60 | class MathInlineParen(Math):
61 | r"""Inline math ``\(...\)``."""
62 | head = r'\('
63 | tail = r'\)'
64 |
65 |
66 | class MathInlineDollar(Math):
67 | r"""Inline math ``$...$``."""
68 | head = tail = r'$'
69 |
70 |
71 | class MathDisplayBracket(Math):
72 | r"""Display math ``\[...\]``."""
73 | head = r'\['
74 | tail = r'\]'
75 |
76 |
77 | class MathDisplayDollar(Math):
78 | r"""Display math ``$$...$$`` (discouraged)."""
79 | head = tail = '$$'
80 |
81 |
82 | class Command(base.BackslashCommand):
83 | """A backslash-prefixed command.
84 |
85 | The backslash is not in the head value, but added on
86 | :meth:`~.element.Element.write`.
87 |
88 | Arguments may be appended as children.
89 |
90 | """
91 |
92 |
93 | class Text(element.TextElement):
94 | """Common text."""
95 |
96 |
97 | class Environment(element.Element):
98 | """A LaTeX environment.
99 |
100 | Starts with a :class:`Command` ``\\begin``, with zero or more
101 | :class:`Option` nodes, then an :class:`EnvironmentName`; then the contents,
102 | and finally an ``\\end`` :class:`Command` with and again an
103 | :meth:`EnvironmentName`.
104 |
105 | """
106 | space_before = space_after = '\n'
107 |
108 | @classmethod
109 | def with_name(cls, name, *children, **kwargs):
110 | """Convenience method to create an :class:`Environment`.
111 |
112 | Zero or more child nodes can be specified, and keyboard arguments
113 | are given to the constructor.
114 |
115 | """
116 | return cls(
117 | Command('begin', EnvironmentName(name), space_after='\n'),
118 | *children,
119 | Command('end', EnvironmentName(name), space_before='\n'), **kwargs)
120 |
121 |
122 | class EnvironmentName(element.TextElement):
123 | """The name of an environment.
124 |
125 | The name is in the head value, the braces are added on :meth:`write`.
126 |
127 | """
128 | @classmethod
129 | def read_head(cls, origin):
130 | for t in origin:
131 | if t.text not in "{}":
132 | return t.text
133 |
134 | def write_head(self):
135 | return '{' + self.head + '}'
136 |
137 |
138 | class Comment(base.SinglelineComment):
139 | r"""A singleline comment after ``%``."""
140 | space_after = '\n'
141 |
142 | @classmethod
143 | def read_head(cls, origin):
144 | return ''.join(t.text for t in origin[1:])
145 |
146 | def write_head(self):
147 | return '%{}'.format(self.head)
148 |
149 |
150 |
--------------------------------------------------------------------------------
/quickly/dom/util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Some utility functions.
23 | """
24 |
25 | def whitespace_key(text):
26 | r"""Return a key to determine the importance of the whitespace.
27 |
28 | This is used by e.g. the :func:`collapse_whitespace` function. A two-tuple
29 | is returned: ``(newlines, spaces)``, where the first value is the number of
30 | newlines in the text, and the second value the number of spaces.
31 |
32 | """
33 | return text.count('\n'), text.count(' ')
34 |
35 |
36 | def collapse_whitespace(whitespaces):
37 | r"""Return the "most important" whitespace of the specified strings.
38 |
39 | This is used to combine whitespace requirements. For example, newlines
40 | are preferred over single spaces, and a single space is preferred over
41 | an empty string. For example::
42 |
43 | >>> collapse_whitespace(['\n', ' '])
44 | '\n'
45 | >>> collapse_whitespace([' ', ''])
46 | ' '
47 |
48 | """
49 | return max(whitespaces, key=whitespace_key, default='')
50 |
51 |
52 | def combine_text(fragments):
53 | r"""Concatenate text fragments collapsing whitespace before and after the
54 | fragments.
55 |
56 | ``fragments`` is an iterable of (``before``, ``text``, ``after``) tuples,
57 | where ``before`` and ``after`` are whitespace. If a ``text`` is empty, the
58 | whitespace before and after are collapsed into the other surrounding
59 | whitespace. Returns a tree-tuple (``before``, ``text``, ``after``)
60 | containing the first ``before`` value, the combined ``text``, and the last
61 | ``after`` value.
62 |
63 | """
64 | result = []
65 | whitespace = []
66 | for before, text, after in fragments:
67 | whitespace.append(before)
68 | if text:
69 | result.append(collapse_whitespace(whitespace))
70 | result.append(text)
71 | whitespace.clear()
72 | whitespace.append(after)
73 | return ''.join(result[:1]), ''.join(result[1:]), collapse_whitespace(whitespace)
74 |
75 |
76 | def add_newlines(node, text, block_separator='\n', max_blank_lines=10):
77 | """Set whitespace properties of the node and all its descendents according
78 | to the original text.
79 |
80 | Only nodes with an origin are affected. When a node appears on a new line,
81 | the ``space_before`` property is set; when the tail part of a node appears
82 | on a new line, the ``space_before_tail`` property is set.
83 |
84 | It is possible to set the ``block_separator`` (by default a single
85 | newline); and the maximum amount of consecutive blank lines, using
86 | ``max_blank_lines``.
87 |
88 | This can be useful before re-indenting or reformatting a document
89 | completely, to retain some aspects of the original formatting.
90 |
91 | """
92 | def next_block():
93 | nonlocal current_block
94 | if current_block < len(text):
95 | current_block = text.find(block_separator, current_block + 1)
96 | if current_block == -1:
97 | current_block = len(text)
98 | return True
99 | return False
100 |
101 | def get_newlines(pos):
102 | count = 0
103 | while pos > current_block:
104 | if not next_block():
105 | break
106 | count += 1
107 | count = min(count, max_blank_lines)
108 | return '\n' * count
109 |
110 | def handle_node(node):
111 | try:
112 | head_origin = node.head_origin
113 | except AttributeError:
114 | pass
115 | else:
116 | s = get_newlines(head_origin[0].pos)
117 | if s and whitespace_key(s) > whitespace_key(node.space_before):
118 | node.space_before = s
119 |
120 | for n in node:
121 | handle_node(n)
122 |
123 | try:
124 | tail_origin = node.tail_origin
125 | except AttributeError:
126 | pass
127 | else:
128 | s = get_newlines(tail_origin[0].pos)
129 | if s and whitespace_key(s) > whitespace_key(node.space_before_tail):
130 | node.space_before_tail = s
131 |
132 | current_block = -1
133 | next_block()
134 | handle_node(node)
135 |
136 |
137 | def replace_unknown(tree, text):
138 | """Replace all :class:`~.base.Unknown` nodes with :class:`~.base.Text` nodes
139 | containing the respective text.
140 |
141 | The ``text`` should be the text the DOM ``tree`` was generated from. Do
142 | this before writing out a node using :meth:`~.element.Element.write` or
143 | :meth:`~.element.Element.write_indented` or related methods.
144 |
145 | """
146 | from . import base
147 | for n in tree // base.Unknown:
148 | t = base.Text(text[n.pos:n.end])
149 | t.copy_origin_from(n, True)
150 | n.replace_with(t)
151 |
152 |
153 | def skip_comments(node):
154 | """Yield child nodes of the DOM node, skipping inheritants of base.Comment."""
155 | from .base import Comment
156 | return (n for n in node if not isinstance(n, Comment))
157 |
158 |
159 | def pop_comments(node):
160 | """Pop and return comments off the end of node (and/or its children)."""
161 | from .base import Comment
162 | comments = []
163 | while len(node):
164 | if isinstance(node[-1], Comment):
165 | comments.append(node.pop())
166 | else:
167 | node = node[-1]
168 | comments.reverse()
169 | return comments
170 |
171 |
172 | def lilypond_version(node):
173 | """Return the LilyPond version in the node's document as a tuple of ints.
174 |
175 | Returns the empty tuple if the version is not set.
176 |
177 | """
178 | from . import lily
179 | for v in node.root() // lily.Version:
180 | return v.version
181 | return ()
182 |
183 |
184 |
--------------------------------------------------------------------------------
/quickly/duration.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2021 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Functions to deal with LilyPond's musical durations.
23 |
24 | A duration is a :class:`~fractions.Fraction` or an integer, where a whole note
25 | is 1. A duration can be split in two values, log and dot-count, where the log
26 | value is 0 for a whole note, 1 for a half note, 2 for a crotchet, -1 for a
27 | ``\\breve``, etc. This is the same way LilyPond handles durations.
28 |
29 | Durations can be scaled using multiplying, e.g. with a Fraction.
30 |
31 | """
32 |
33 | import fractions
34 | import math
35 |
36 |
37 | NAMED_DURATIONS = ('breve', 'longa', 'maxima')
38 |
39 |
40 | class Transform:
41 | """Combine modifications of a duration (shift and/or scale).
42 |
43 | Use it to calculate the real length of musical items. Transforms can be
44 | added. A Transform that doesn't modify anything evaluates to False.
45 |
46 | """
47 | def __init__(self, log=0, dotcount=0, scale=1):
48 | self.log = log #: the log to shift
49 | self.dotcount = dotcount #: the dots to shift
50 | self.scale = scale #: the scaling
51 |
52 | def __bool__(self):
53 | return bool(self.log or self.dotcount or self.scale != 1)
54 |
55 | def __repr__(self):
56 | return "<{} log={} dotcount={} scale={}>".format(
57 | type(self).__name__, self.log, self.dotcount, self.scale)
58 |
59 | def __add__(self, other):
60 | log = self.log + other.log
61 | dotcount = self.dotcount + other.dotcount
62 | scale = self.scale * other.scale
63 | return type(self)(log, dotcount, scale)
64 |
65 | def length(self, duration, scaling=1):
66 | """Return the actual musical length of the duration and scaling values."""
67 | duration, scaling = self.transform(duration, scaling)
68 | return duration * scaling
69 |
70 | def transform(self, duration, scaling=1):
71 | """Return a transformed two-tuple (duration, scaling)."""
72 | if self.log or self.dotcount:
73 | duration = shift_duration(duration, self.log, self.dotcount)
74 | return duration, scaling * self.scale
75 |
76 |
77 | def log_dotcount(value):
78 | r"""Return the integer two-tuple (log, dotcount) for the duration value.
79 |
80 | The ``value`` may be a Fraction, integer or floating point value.
81 |
82 | The returned log is 0 for a whole note, 1 for a half note, 2 for a
83 | crotchet, -1 for a ``\\breve``, etc. This is the same way LilyPond handles
84 | durations. For example::
85 |
86 | >>> from quickly.duration import log_dotcount
87 | >>> log_dotcount(1)
88 | (0, 0)
89 | >>> log_dotcount(1/2)
90 | (1, 0)
91 | >>> log_dotcount(4)
92 | (-2, 0)
93 | >>> log_dotcount(1/4)
94 | (2, 0)
95 | >>> log_dotcount(3/4)
96 | (1, 1)
97 | >>> log_dotcount(7/16)
98 | (2, 2)
99 | >>> to_string(duration(*log_dotcount(Fraction(3, 4))))
100 | '2.'
101 |
102 | The value is truncated to a duration that can be expressed by a note length
103 | and a number of dots. For example::
104 |
105 | >>> to_string(duration(*log_dotcount(1)))
106 | '1'
107 | >>> to_string(duration(*log_dotcount(1.4)))
108 | '1'
109 | >>> to_string(duration(*log_dotcount(1.5)))
110 | '1.'
111 | >>> to_string(duration(*log_dotcount(0.9999)))
112 | '2............'
113 |
114 | """
115 | mantisse, exponent = math.frexp(value)
116 | log = 1 - exponent
117 | m, e = math.frexp(1 - mantisse)
118 | dotcount = -e - (m > 0.5)
119 | return log, dotcount
120 |
121 |
122 | def duration(log, dotcount=0):
123 | r"""Return the duration as a Fraction.
124 |
125 | See for an explanation of the ``log`` and ``dotcount`` values
126 | :func:`log_dotcount`.
127 |
128 | """
129 | numer = ((2 << dotcount) - 1) << 3
130 | denom = 1 << (dotcount + log + 3)
131 | return fractions.Fraction(numer, denom)
132 |
133 |
134 | def shift_duration(value, log, dotcount=0):
135 | r"""Shift the duration.
136 |
137 | This function is analogous to LilyPond's ``\shiftDurations`` command. It
138 | modifies a duration by shifting the log and the number of dots. Adding 1 to
139 | the log halves the note length, and adding a dot mutiplies the note length
140 | with ``(1 + 1/2**)``. Subtracting 1 from the log doubles the note
141 | length.
142 |
143 | The ``value`` should be a duration that is expressable by a note length and
144 | a number of dots. A Fraction is returned. ``log`` is the scaling as a power
145 | of 2; and ``dotcount`` the number of dots to be added (or removed, by
146 | specifying a negative value).
147 |
148 | For example::
149 |
150 | >>> shift_duration(Fraction(1, 8), -1)
151 | Fraction(1, 4)
152 | >>> shift_duration(Fraction(1, 8), -2)
153 | Fraction(1, 2)
154 | >>> shift_duration(Fraction(1, 4), 1, 1)
155 | Fraction(3, 16)
156 | >>> to_string(shift_duration(Fraction(1,4), 1, 1))
157 | '8.'
158 | >>> to_string(shift_duration(from_string('2'), 1, 1))
159 | '4.'
160 | >>> shift_duration(Fraction(7, 8), 0, -2)
161 | Fraction(1, 2)
162 | >>> shift_duration(Fraction(7, 8), 0, -1)
163 | Fraction(3, 4)
164 |
165 | The number of dots in a duration will never drop below zero::
166 |
167 | >>> shift_duration(1/4, 0, -1)
168 | Fraction(1, 4)
169 |
170 | """
171 | old_log, old_dotcount = log_dotcount(value)
172 | return duration(old_log + log, max(0, old_dotcount + dotcount))
173 |
174 |
175 | def to_string(value):
176 | r"""Convert the value (most times a Fraction) to a LilyPond string notation.
177 |
178 | The value is truncated to a duration that can be expressed by a note length
179 | and a number of dots. For example::
180 |
181 | >>> from fractions import Fraction
182 | >>> from quickly.duration import to_string
183 | >>> to_string(Fraction(3, 2))
184 | '1.'
185 | >>> to_string(4)
186 | '\\longa'
187 | >>> to_string(7.75)
188 | '\\longa....'
189 |
190 | Raises an IndexError if the base duration is too long (longer than
191 | ``\maxima``).
192 |
193 | """
194 | log, dotcount = log_dotcount(value)
195 | if log < 0:
196 | dur = '\\' + NAMED_DURATIONS[-1-log]
197 | else:
198 | dur = 1 << log
199 | return '{}{}'.format(dur, '.' * dotcount)
200 |
201 |
202 | def from_string(text, dotcount=None):
203 | r"""Convert a LilyPond duration string (e.g. ``'4.'``) to a Fraction.
204 |
205 | The durations ``\breve``, ``\longa`` and ``\maxima`` may be used with or
206 | without backslash. If ``dotcount`` is None, the dots are expected to be in
207 | the ``text``.
208 |
209 | For example::
210 |
211 | >>> from quickly.duration import from_string
212 | >>> from_string('8')
213 | Fraction(1, 8)
214 | >>> from_string('8..')
215 | Fraction(7, 32)
216 | >>> from_string('8', dotcount=2)
217 | Fraction(7, 32)
218 |
219 | Raises a ValueError if an invalid duration is specified.
220 |
221 | """
222 | if dotcount is None:
223 | dotcount = text.count('.')
224 | text = text.strip(' \t.')
225 | try:
226 | log = int(text).bit_length() - 1
227 | except ValueError:
228 | log = -1 - NAMED_DURATIONS.index(text.lstrip('\\'))
229 | return duration(log, dotcount)
230 |
231 |
232 | def is_writable(value):
233 | """Return True if the value can be exactly expressed in a log and dotcount
234 | value, without loss.
235 |
236 | The value should be >= 1/1024 and < 16, because LilyPond can't display more
237 | than 8 flags and ``\\maxima`` is the longest available duration name.
238 |
239 | """
240 | mantisse, exponent = math.frexp(value)
241 | return -10 < exponent < 5 and math.frexp(1 - mantisse)[0] == 0.5
242 |
243 |
--------------------------------------------------------------------------------
/quickly/lang/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Language definitions, thay may or may not inherit from *parce*'s language
23 | definitions. Also the corresponding Transform classes are defined in these
24 | modules.
25 |
26 | """
27 |
28 |
--------------------------------------------------------------------------------
/quickly/lang/latex.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | LaTeX language and transformation definition.
23 | """
24 |
25 | import itertools
26 |
27 | from parce import skip, lexicon, default_target
28 | from parce.rule import bygroup, ifarg, ifeq, ifgroup
29 | import parce.lang.tex
30 | import parce.action as a
31 |
32 | from quickly.dom import base, element, lily, scm, tex
33 | from . import lilypond
34 |
35 |
36 | class Latex(parce.lang.tex.Latex):
37 | """Latex language definition."""
38 | @classmethod
39 | def get_environment_target(cls, name):
40 | return ifeq(name, "lilypond",
41 | (lilypond.LilyPond.latex_lilypond_environment, cls.test_lilypond_option),
42 | super().get_environment_target(name))
43 |
44 | @classmethod
45 | def common(cls):
46 | yield r'(\\lilypond)\s*(?:(\{)|(\[))?', bygroup(a.Name.Builtin, a.Delimiter.Brace, a.Delimiter), \
47 | ifgroup(2, lilypond.LilyPond.latex_lilypond_environment('short form'),
48 | ifgroup(3, cls.option("lilypond")))
49 | yield from super().common()
50 |
51 | @lexicon
52 | def option(cls):
53 | yield ifarg(r'(\])\s*(\{)'), bygroup(a.Delimiter, a.Delimiter.Brace), -1, \
54 | lilypond.LilyPond.latex_lilypond_environment('short form')
55 | yield from super().option
56 | yield r'\[', a.Delimiter.Bracket # this can match if we were here looking for a [
57 |
58 | @lexicon
59 | def test_lilypond_option(cls):
60 | """One time check for Latex options at the beginning of a LilyPond environment.
61 |
62 | This lexicon never creates a context.
63 |
64 | """
65 | yield r'(?=\s*\[)', skip, -1, cls.option
66 | yield default_target, -1
67 |
68 |
69 | class LatexTransform(base.Transform):
70 | """Transform Latex quickly.dom."""
71 | ## Transform methods
72 | def root(self, items):
73 | """Process the ``root`` context."""
74 | return tex.Document(*self.common(items))
75 |
76 | def brace(self, items):
77 | """Process the ``brace`` context; returns a Brace node."""
78 | head = items[:1]
79 | tail = (items.pop(),) if items[-1] == '}' else ()
80 | return self.factory(tex.Brace, head, tail, *self.common(items[1:]))
81 |
82 | def option(self, items):
83 | r"""Process the ``option`` context.
84 |
85 | Returns a two-tuple(options, head_origin).
86 |
87 | The options is a list of objects that are finished Option nodes, only the
88 | first might be incomplete because of a missing opening token. In that
89 | case the first object is a tuple (contents, tail_origin). (The
90 | tail_origin might be empty in the case of an imcomplete source text).
91 |
92 | The head_origin is normally empty, only for the ``\lilypond[opts]{``
93 | command, it is the brace that starts the braced expression.
94 |
95 | """
96 | command_head = ()
97 | if items.peek(-1, a.Delimiter.Brace):
98 | # opening bracket of short form LilyPond command
99 | command_head = (items.pop(),)
100 | i = 0
101 | if items.peek(0, a.Delimiter.Bracket) and items[0].text == '[':
102 | # complete first Option node
103 | head_origin = items[0:1]
104 | i = 1
105 | else:
106 | head_origin = ()
107 | nodes = []
108 | pos = i
109 | while True:
110 | try:
111 | i = items.index(']', pos)
112 | except ValueError:
113 | nodes.append(self.factory(tex.Option, head_origin, (), *self.common(items[pos:])))
114 | break
115 | tail_origin = items[i],
116 | if head_origin:
117 | # append complete node
118 | nodes.append(self.factory(tex.Option, head_origin, tail_origin, *self.common(items[pos:i])))
119 | else:
120 | # append contents and tail origin, head is in a previous context
121 | nodes.append( (self.common(items[pos:i]), items[i:i+1]) )
122 | pos = i + 1
123 | # another Option node?
124 | try:
125 | i = items.index('[', pos)
126 | except ValueError:
127 | break
128 | pos = i + 1
129 | head_origin = items[i:pos]
130 | return nodes, command_head
131 |
132 | def environment_option(self, items):
133 | """Process the ``environment_option`` context.
134 |
135 | Returns a tuple(option, envname), where *option* is a list like the
136 | first value returned by :meth:`option` and *envname* an
137 | :class:`~quickly.dom.tex.EnvironmentName` node it it was there at the
138 | end of the options list, otherwise None.
139 |
140 | """
141 | env_name = None
142 | if items.peek(-3, a.Delimiter, a.Name.Tag, a.Delimiter):
143 | # environment name at end
144 | env_name = self.factory(tex.EnvironmentName, items[-3:])
145 | del items[-3:]
146 | return self.option(items)[0], env_name
147 |
148 | def environment_math(self, items):
149 | """Process the ``environment_math`` context."""
150 | return self.environment(items)
151 |
152 | def environment(self, items):
153 | r"""Process the ``environment`` context.
154 |
155 | Returns a list of nodes, the last is the ``\end`` command.
156 |
157 | """
158 | end = None
159 | if items.peek(-4, a.Name.Builtin, a.Delimiter, a.Name.Tag, a.Delimiter):
160 | end = self.factory(tex.Command, items[-4:-3])
161 | end.append(self.factory(tex.EnvironmentName, items[-3:]))
162 | items = items[:-4]
163 | nodes = self.common(items)
164 | if end:
165 | nodes.append(end)
166 | return nodes
167 |
168 | _math_mapping = element.head_mapping(
169 | tex.MathInlineParen, tex.MathInlineDollar, tex.MathDisplayBracket,
170 | tex.MathDisplayDollar)
171 |
172 | def math(self, items):
173 | """Process the ``math`` context; return a Math node."""
174 | head = items[:1]
175 | cls = self._math_mapping[head[0].text]
176 | tail = (items.pop(),) if len(items) > 1 and items[-1] == cls.tail else ()
177 | return self.factory(cls, head, tail, *self.common(items[1:]))
178 |
179 | def comment(self, items):
180 | """Process the ``comment`` context."""
181 | return self.factory(tex.Comment, items)
182 |
183 | test_lilypond_option = None # never creates content
184 |
185 | ## Helper methods
186 | def common(self, items):
187 | """Compose and yield Latex nodes; used in most contexts."""
188 | result = []
189 | text = []
190 | z = len(items)
191 | i = 0
192 |
193 | def get_options(options):
194 | """Yield Option nodes; i must be at the ``[``."""
195 | if isinstance(options[0], tuple):
196 | head = items[i:i+1]
197 | contents, tail = options[0]
198 | yield self.factory(tex.Option, head, tail, *contents)
199 | options = options[1:]
200 | yield from options
201 |
202 | def command(t):
203 | """Return a generic Command node. Arguments are appended as child."""
204 | nonlocal i
205 | cmd = self.factory(tex.Command, (t,))
206 | # options? append as child
207 | if items.peek(i, a.Delimiter.Bracket, "option"):
208 | cmd.extend(get_options(items[i+1].obj[0]))
209 | i += 2
210 | # braced piece of text? append as child
211 | if items.peek(i, "brace"):
212 | cmd.append(items[i].obj)
213 | i += 1
214 | return cmd
215 |
216 | def lilypond_command(t):
217 | r"""Return the Command node for a \lilypond { ... } command."""
218 | nonlocal i
219 | cmd = self.factory(tex.Command, (t,))
220 | if items.peek(i, a.Delimiter.Brace, 'latex_lilypond_environment'):
221 | options, music, tail = items[i+1].obj
222 | cmd.append(self.factory(tex.Brace, items[i:i+1], tail, *music))
223 | i += 2
224 | elif items.peek(i, a.Delimiter, 'option'):
225 | options, cmd_head = items[i+1].obj
226 | cmd.extend(get_options(options))
227 | i += 2
228 | if items.peek(i, 'latex_lilypond_environment'):
229 | options, music, tail = items[i].obj
230 | cmd.append(self.factory(tex.Brace, cmd_head, tail, *music))
231 | i += 1
232 | return cmd
233 |
234 | def environment(t):
235 | """Return an Environment node, possibly containing LilyPond music."""
236 | nonlocal i
237 | env = tex.Environment(self.factory(tex.Command, (t,)))
238 | if items.peek(i, a.Delimiter, a.Name.Tag, a.Delimiter):
239 | # no options, add the name
240 | env[-1].append(self.factory(tex.EnvironmentName, items[i:i+3]))
241 | i += 3
242 | elif items.peek(i, a.Delimiter.Bracket, "environment_option"):
243 | # environment options
244 | options, env_name = items[i+1].obj
245 | env[-1].extend(get_options(options))
246 | if env_name:
247 | env[-1].append(env_name)
248 | i += 2
249 | # now add the environment
250 | if i < z and not items[i].is_token:
251 | if items[i].name in ('environment', 'environment_math'):
252 | env.extend(items[i].obj)
253 | elif items[i].name == 'latex_lilypond_environment':
254 | options, music, tail = items[i].obj
255 | env[-1].extend(options)
256 | env.extend(music)
257 | else:
258 | print("unknown Latex environment:", items[i].name) #TEMP
259 | i += 1
260 | return env
261 |
262 | while i < z:
263 | node = None
264 | t = items[i]
265 | i += 1
266 | if t.is_token:
267 | if t.action is a.Name.Builtin:
268 | if t == r'\lilypond':
269 | node = lilypond_command(t)
270 | else: # if t -- r'\begin':
271 | node = environment(t)
272 | elif t.action is a.Name.Command:
273 | node = command(t)
274 | else:
275 | text.append(t)
276 | # t is a context result
277 | elif isinstance(t.obj, element.Element):
278 | node = t.obj
279 | else:
280 | print("unknown object:", t.name) # TEMP
281 | if node:
282 | if text:
283 | result.append(self.factory(tex.Text, text))
284 | text = []
285 | result.append(node)
286 | if text:
287 | result.append(self.factory(tex.Text, text))
288 | return result
289 |
290 |
291 |
292 | class LatexAdHocTransform(base.AdHocTransform, LatexTransform):
293 | """LatexTransform that does not keep the origin tokens."""
294 | pass
295 |
296 |
297 |
--------------------------------------------------------------------------------
/quickly/lang/scheme.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Scheme language and transformation definition.
23 | """
24 |
25 | import itertools
26 |
27 | import parce.lang.scheme
28 | import parce.action as a
29 |
30 | from quickly.dom import base, element, lily, scm
31 |
32 |
33 | class Scheme(parce.lang.scheme.SchemeLily):
34 | """Scheme language definition."""
35 | @classmethod
36 | def common(cls, pop=0):
37 | from . import lilypond
38 | yield r"#{", a.Bracket.LilyPond.Start, pop, lilypond.LilyPond.schemelily
39 | yield from super(parce.lang.scheme.SchemeLily, cls).common(pop)
40 |
41 |
42 | class SchemeTransform(base.Transform):
43 | """Transform Scheme quickly.dom."""
44 | # mapping is used in common, below
45 | _common_mapping = {
46 | a.Character: scm.Char,
47 | a.Delimiter.Dot: scm.Dot,
48 | a.Keyword: scm.Identifier,
49 | a.Name: scm.Identifier,
50 | a.Literal.Number.Boolean: scm.Bool,
51 | }
52 |
53 | ## helper method
54 | def common(self, items):
55 | """Yield dom nodes from tokens."""
56 | quotes = []
57 | def nodes():
58 | for i in items:
59 | node = None
60 | if i.is_token:
61 | if i.action == a.Delimiter.Scheme.Quote:
62 | quotes.append(i)
63 | else:
64 | yield self.factory(self._common_mapping[i.action], (i,))
65 | elif isinstance(i.obj, element.Element):
66 | yield i.obj
67 | for node in nodes():
68 | for q in reversed(quotes):
69 | node = self.factory(scm.Quote, (q,), (), node)
70 | quotes.clear()
71 | yield node
72 |
73 | ### transforming methods
74 | def root(self, items):
75 | """Build a full ``scm.Document``."""
76 | return scm.Document(*self.common(items))
77 |
78 | def list(self, items):
79 | """Build a scm.List ``(`` ... ``)``."""
80 | head = items[:1]
81 | tail = (items.pop(),) if items[-1] == ')' else ()
82 | return self.factory(scm.List, head, tail, *self.common(items[1:]))
83 |
84 | def vector(self, items):
85 | """Build a scm.Vector ``#(`` ... ``)``."""
86 | head = items[:1]
87 | tail = (items.pop(),) if items[-1] == ')' else ()
88 | return self.factory(scm.Vector, head, tail, *self.common(items[1:]))
89 |
90 | _radix_mapping = {
91 | 2: scm.Bin,
92 | 8: scm.Oct,
93 | 10: scm.Number,
94 | 16: scm.Hex,
95 | }
96 | def number(self, items):
97 | """Create a Number node."""
98 | radix = items.arg or 10
99 | try:
100 | return self.factory(self._radix_mapping[radix], items)
101 | except (ValueError, ZeroDivisionError):
102 | return self.factory(scm.NaN, items)
103 |
104 | def string(self, items):
105 | """Create a String node."""
106 | return self.factory(scm.String, items)
107 |
108 | def multiline_comment(self, items):
109 | """Create a MultilineComment node."""
110 | return self.factory(scm.MultilineComment, items)
111 |
112 | def singleline_comment(self, items):
113 | """Create a SinglelineComment node."""
114 | return self.factory(scm.SinglelineComment, items)
115 |
116 | def scheme(self, items):
117 | """Create a Scheme node in LilyPond."""
118 | head = items[:1] # $, #, $@ or #@ token introducing scheme mode
119 | scheme = self.factory(lily.Scheme, head)
120 | for i in self.common(items[1:]):
121 | scheme.append(i)
122 | break
123 | return scheme
124 |
125 | def argument(self, items):
126 | """One scheme object, from within LilyPond."""
127 | for i in self.common(items):
128 | return i
129 |
130 |
131 | class SchemeAdHocTransform(base.AdHocTransform, SchemeTransform):
132 | """SchemeTransform that does not keep the origin tokens."""
133 | pass
134 |
135 |
--------------------------------------------------------------------------------
/quickly/numbering.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2021 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Functions dealing with numbering of voices, parts etcetera.
23 | """
24 |
25 |
26 | import string
27 |
28 |
29 |
30 | def int2roman(n):
31 | """Convert an integer value to a roman number string.
32 |
33 | E.g. 1 -> "I", 12 -> "XII", 2015 -> "MMXV"
34 |
35 | ``n`` has to be an integer >= 1; raises ValueError otherwise.
36 |
37 | """
38 | if n < 1:
39 | raise ValueError('Roman numerals must be positive integers, got %s' % n)
40 |
41 | roman_numerals = (
42 | ("M", 1000), ("CM", 900), ("D", 500), ("CD", 400),
43 | ("C", 100), ("XC", 90), ("L", 50), ("XL", 40), ("X", 10), ("IX", 9), ("V", 5),
44 | ("IV", 4), ("I", 1),
45 | )
46 |
47 | roman = []
48 | for char, num in roman_numerals:
49 | k, n = divmod(n, num)
50 | roman.append(char * k)
51 | return "".join(roman)
52 |
53 |
54 | def roman2int(s):
55 | """Convert a string with a roman numeral to an integer.
56 |
57 | E.g. "MCMLXVII" -> 1967, "iii" -> 3
58 |
59 | Raises a KeyError on invalid characters.
60 |
61 | """
62 |
63 | roman_numerals = {'I':1, 'V':5, 'X':10, 'L':50, 'C':100, 'D':500, 'M':1000 }
64 | num = prev = 0
65 | for char in reversed(s.upper()):
66 | val = roman_numerals[char]
67 | if val < prev:
68 | num -= val
69 | continue
70 | prev = val
71 | num += val
72 | return num
73 |
74 |
75 | def int2letter(n, chars=string.ascii_uppercase):
76 | """Convert an integer to one or more letters.
77 |
78 | E.g. 1 -> "A", 2 -> "B", ... 26 -> "Z", 27 -> "AA", etc.
79 | Zero returns the empty string.
80 |
81 | chars is the string to pick characters from, defaulting to
82 | ``string.ascii_uppercase``.
83 |
84 | """
85 | mod = len(chars)
86 | result = []
87 | while n > 0:
88 | n, c = divmod(n - 1, mod)
89 | result.append(c)
90 | return "".join(chars[c] for c in reversed(result))
91 |
92 |
93 | def letter2int(s, chars=string.ascii_uppercase):
94 | """Convert a string with letters to an integer.
95 |
96 | E.g. "AA" -> 27
97 |
98 | An empty string yields 0. Raises a ValueError when a character is not
99 | available in ``chars`` (which defaults to ``string.ascii_uppercase``).
100 |
101 | """
102 | mod = len(chars)
103 | result = 0
104 | for char in s:
105 | result *= mod
106 | result += chars.index(char) + 1
107 | return result
108 |
109 |
110 | def int2text(n):
111 | """Convert an integer to the English language name of that integer.
112 |
113 | E.g. converts 1 to "One". Supports numbers 0 to 999999999.
114 | This can be used in LilyPond identifiers (that do not support digits).
115 |
116 | """
117 | from parce.lang.numbers import ENGLISH_TO19, ENGLISH_TENS
118 | def _int2text(n):
119 | for fact, name in ((1000000, 'million'), (1000, 'thousand')):
120 | if n >= fact:
121 | count, n = divmod(n, fact)
122 | yield from _int2text(count)
123 | yield name
124 | if n >= 100:
125 | tens, n = divmod(n, 100)
126 | yield ENGLISH_TO19[tens]
127 | yield "hundred"
128 | if n >= 20:
129 | tens, n = divmod(n, 10)
130 | yield ENGLISH_TENS[tens-2]
131 | if n:
132 | yield ENGLISH_TO19[n]
133 | return "".join(t.title() for t in _int2text(n)) or 'Zero'
134 |
135 |
136 | def text2int(s):
137 | """Convert a text number in English language to an integer.
138 |
139 | E.g. "TwentyOne" -> 21, 'three' -> 3
140 |
141 | Ignores preceding other text. Returns 0 if no valid text is found.
142 |
143 | """
144 | from parce.lang.numbers import English
145 | from parce.transform import transform_text
146 | for n in transform_text(English.root, s):
147 | return n
148 | return 0
149 |
150 |
--------------------------------------------------------------------------------
/quickly/pkginfo.py:
--------------------------------------------------------------------------------
1 | # This file is part of python-ly, https://pypi.python.org/pypi/python-ly
2 | #
3 | # Copyright (c) 2014 - 2015 by Wilbert Berendsen
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 | # See http://www.gnu.org/licenses/ for more information.
19 |
20 | """
21 | Meta-information about the quickly package.
22 |
23 | This information is used by the install script.
24 |
25 | The :attr:`version` and :attr:`version_string` are also available
26 | in the global :mod:`quickly` module space.
27 |
28 | """
29 |
30 | import collections
31 | Version = collections.namedtuple("Version", "major minor patch")
32 |
33 |
34 | #: name of the package
35 | name = "quickly"
36 |
37 | #: the current version
38 | version = Version(0, 7, 0)
39 | version_suffix = ""
40 | #: the current version as a string
41 | version_string = "{}.{}.{}".format(*version) + version_suffix
42 |
43 | #: short description
44 | description = "Tool and library for manipulating LilyPond files"
45 |
46 | #: long description
47 | long_description = \
48 | "The quickly package provides a Python library and a commandline tool " \
49 | "that can be used to parse and manipulate LilyPond source files."
50 |
51 | #: maintainer name
52 | maintainer = "Wilbert Berendsen"
53 |
54 | #: maintainer email
55 | maintainer_email = "info@frescobaldi.org"
56 |
57 | #: homepage
58 | url = "https://quick-ly.info/"
59 |
60 | #: license
61 | license = "GPL v3"
62 |
63 | #: copyright year
64 | copyright_year = "2020-2023"
65 |
66 |
--------------------------------------------------------------------------------
/quickly/relative.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2021-2021 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Utilities and functions to manipulate music written in relative pitches.
23 | """
24 |
25 | from .dom import edit, lily, util
26 | from .pitch import Pitch, PitchProcessor
27 |
28 |
29 | class Rel2abs(edit.Edit):
30 | r"""Convert ``\relative`` music to absolute.
31 |
32 | Removes the ``\relative`` command and makes all pitches absolute.
33 |
34 | :attr:`processor`: a :class:`~.pitch.PitchProcessor`; a default one is used
35 | if none is specified.
36 |
37 | :attr:`first_pitch_absolute`: if True, the first pitch in a ``\relative``
38 | expression is considered to be absolute, when a startpitch is not given.
39 | This is LilyPond >= 2.18 behaviour. If False, the first pitch in a
40 | ``\relative`` expression is considered to be relative to c', if no
41 | startpitch is given. This is LilyPond < 2.18 behaviour. If not specified,
42 | the function looks at the LilyPond version from the document. If the
43 | version can't be determined, defaults to False, the old behaviour.
44 |
45 | You may change these attributes after instantiation.
46 |
47 | """
48 | def __init__(self, processor=None, first_pitch_absolute=None):
49 | #: The :class:`~.pitch.PitchProcessor`; a default one is used if None is specified.
50 | self.processor = processor
51 | #: Whether to consider the first pitch in a ``\relative`` expression absolute
52 | #: if a start pitch is not used (by default dependent on LilyPond version).
53 | self.first_pitch_absolute = first_pitch_absolute
54 |
55 | def edit_range(self, r):
56 | """Do the pitch conversion."""
57 | node = r.ancestor()
58 | processor = self.processor or PitchProcessor()
59 |
60 | if not node.is_root():
61 | processor.find_language(node)
62 |
63 | first_pitch_absolute = self.first_pitch_absolute
64 | if first_pitch_absolute is None:
65 | first_pitch_absolute = util.lilypond_version(node) >= (2, 18)
66 |
67 | def notes(nodes):
68 | """Yield note/rest, chord and octavecheck; follow pitch language."""
69 | for n in processor.follow_language(nodes):
70 | if isinstance(n, lily.Pitchable):
71 | yield n
72 | # music in markup expressions?
73 | for n in node.instances_of(lily.Relative):
74 | make_absolute(n)
75 | elif isinstance(n, (lily.Chord, lily.OctaveCheck)):
76 | yield n
77 | elif isinstance(n, lily.Relative):
78 | make_absolute(n)
79 | elif isinstance(n, (
80 | lily.Inversion, lily.Key, lily.Absolute, lily.Fixed,
81 | lily.Transpose, lily.Transposition, lily.StringTuning,
82 | )):
83 | pass
84 | else:
85 | yield from notes(n)
86 |
87 | def make_absolute(node):
88 | """Convert this relative music to absolute music."""
89 | parent = node.parent
90 | index = parent.index(node)
91 |
92 | nodes = list(util.skip_comments(node))
93 | if len(nodes) > 1 and isinstance(nodes[0], lily.Pitch):
94 | start_note, *nodes = nodes
95 | last_pitch = processor.read_node(start_note)
96 | elif first_pitch_absolute:
97 | last_pitch = Pitch(-1, 3, 0)
98 | else:
99 | last_pitch = Pitch(0, 0, 0)
100 |
101 | parent[index:index+1] = nodes # remove \relative node but keep its child(ren)
102 |
103 | for n in notes(nodes):
104 | if isinstance(n, lily.Pitchable):
105 | # note (or positioned rest)
106 | with processor.process(n) as p:
107 | default_octave = p.octave - n.octave # the octave of the pitch name itself
108 | p.make_absolute(last_pitch, default_octave)
109 | last_pitch = p
110 | elif isinstance(n, lily.Chord):
111 | # chord
112 | stack = [last_pitch]
113 | for note in notes(n):
114 | with processor.process(note) as p:
115 | default_octave = p.octave - note.octave # the octave of the pitch name itself
116 | p.make_absolute(stack[-1], default_octave)
117 | stack.append(p)
118 | last_pitch = stack[:2][-1] # first note of chord, or the old if chord was empty
119 | elif isinstance(n, lily.OctaveCheck):
120 | # OctaveCheck, read last_pitch but remove
121 | for note in notes(n):
122 | last_pitch = processor.read_node(note)
123 | n.parent.remove(n)
124 | # Do it!
125 | nodes = r.instances_of((lily.Language, lily.Include, lily.Relative))
126 | for n in processor.follow_language(nodes):
127 | if isinstance(n, lily.Relative) and r.in_range():
128 | make_absolute(n)
129 |
130 |
131 | def rel2abs(music):
132 | """Convenience function to convert relative music to absolute in music.
133 |
134 | The ``music`` may be a parce document or cursor, a node range or an element
135 | node.
136 |
137 | """
138 | return Rel2abs().edit(music)
139 |
140 |
141 | class Abs2rel(edit.Edit):
142 | r"""Convert music in absolute notation to ``\relative`` notation.
143 |
144 | The topmost :class:`~quickly.dom.lily.MusicList` (``{`` ... ``}`` or ``<<``
145 | ... ``>>``) that has child notes gets a :class:`~quickly.dom.lily.Relative`
146 | parent node.
147 |
148 | :attr:`processor`: a :class:`~.pitch.PitchProcessor`; a default one is used
149 | if none is specified.
150 |
151 | :attr:`start_pitch`: if True, a starting pitch is written after the
152 | ``\relative`` command.
153 |
154 | :attr:`start_pitch`: if False, the :attr:`first_pitch_absolute` attribute
155 | determines the meaning of the first pitch in the new Relative expression.
156 | If :attr:`first_pitch_absolute` is True, the first pitch in the
157 | ``\relative`` expression is considered to be absolute, when a startpitch is
158 | not given. This is LilyPond >= 2.18 behaviour. If False, the first pitch in
159 | a ``\relative`` expression is considered to be relative to c', if no
160 | start pitch is given. This is LilyPond < 2.18 behaviour. If not specified,
161 | the function looks at the LilyPond version from the document. If the
162 | version can't be determined, defaults to False, the old behaviour.
163 |
164 | You may change these attributes after instantiation.
165 |
166 | """
167 | def __init__(self, processor=None, start_pitch=True, first_pitch_absolute=None):
168 | #: The :class:`~.pitch.PitchProcessor`; a default one is used if None is specified.
169 | self.processor = processor
170 | #: Whether to write a starting pitch after the ``\relative`` command.
171 | self.start_pitch = start_pitch
172 | #: Whether to consider the first pitch in a ``\relative`` expression absolute
173 | #: if a start pitch is not used (by default dependent on LilyPond version).
174 | self.first_pitch_absolute = first_pitch_absolute
175 |
176 | def _get_settings(self, node):
177 | """Get preferences for node."""
178 | first_pitch_absolute = self.first_pitch_absolute
179 | if not self.start_pitch and first_pitch_absolute is None:
180 | first_pitch_absolute = util.lilypond_version(node) >= (2, 18)
181 |
182 | processor = self.processor or PitchProcessor()
183 |
184 | if not node.is_root():
185 | processor.find_language(node)
186 |
187 | return processor, first_pitch_absolute
188 |
189 | def edit_range(self, r):
190 | """Do the pitch conversion."""
191 | processor, first_pitch_absolute = self._get_settings(r.ancestor())
192 |
193 | def abs2rel():
194 | """Find MusicList nodes, and if they contain notes, make relative."""
195 | for n in r.instances_of((lily.MusicList, lily.Relative, lily.ChordMode)):
196 | if isinstance(n, lily.MusicList):
197 | if any(n / lily.Pitchable) and r.in_range():
198 | r.node = self._make_relative_internal(r.node, processor, first_pitch_absolute)
199 | else:
200 | abs2rel()
201 | elif isinstance(n, lily.ChordMode):
202 | r.node = self._make_relative_internal(r.node, processor, first_pitch_absolute)
203 |
204 | # Do it!
205 | abs2rel()
206 |
207 | def make_relative(self, node):
208 | """Make al notes and pitched rests in the specified :class:`~quickly.dom.lily.MusicList` or
209 | :class:`~quickly.dom.lily.SimultaneousMusicList` node relative.
210 |
211 | Returns a :class:`~quickly.dom.lily.Relative` node with the modified
212 | music list appended.
213 |
214 | Replace the node in its parent with the returned node if desired.
215 |
216 | """
217 | processor, first_pitch_absolute = self._get_settings(node)
218 | return self._make_relative_internal(node, processor, first_pitch_absolute)
219 |
220 | def _make_relative_internal(self, node, processor, first_pitch_absolute):
221 | """Implementation of make_relative() with settings."""
222 | rel = lily.Relative()
223 |
224 | def get_first_pitch(p):
225 | if self.start_pitch:
226 | last_pitch = Pitch(p.octave, 0, 0)
227 | if p.note > 3:
228 | last_pitch.octave += 1
229 | rel.append(processor.pitchable(last_pitch, lily.Pitch))
230 | elif first_pitch_absolute:
231 | last_pitch = Pitch(-1, 3, 0)
232 | else:
233 | last_pitch = Pitch(0, 0, 0)
234 | return last_pitch
235 |
236 | def relative_note(node, last_pitch):
237 | with processor.process(node) as p:
238 | default_octave = p.octave - node.octave
239 | if last_pitch is None:
240 | last_pitch = get_first_pitch(p)
241 | lp = p.copy()
242 | p.make_relative(last_pitch, default_octave)
243 | return lp
244 |
245 | def relative_notes(node, last_pitch=None):
246 | for n in processor.follow_language(node):
247 | if isinstance(n, lily.Pitchable):
248 | last_pitch = relative_note(n, last_pitch)
249 | elif isinstance(n, lily.Chord):
250 | stack = [last_pitch]
251 | for note in n / lily.Pitchable:
252 | stack.append(relative_note(note, last_pitch))
253 | last_pitch = stack[:2][-1] # first of chord or old if empty
254 | elif isinstance(n, (
255 | lily.Inversion, lily.Key, lily.Absolute, lily.Fixed,
256 | lily.Transpose, lily.Transposition, lily.StringTuning,
257 | )):
258 | pass
259 | else:
260 | last_pitch = relative_notes(n, last_pitch)
261 | return last_pitch
262 |
263 | relative_notes(node)
264 | rel.append(node)
265 | return rel
266 |
267 |
268 | def abs2rel(music):
269 | """Convenience function to convert absolute music to relative.
270 |
271 | The ``music`` may be a parce document or cursor, a node range or an element
272 | node.
273 |
274 | """
275 | return Abs2rel().edit(music)
276 |
277 |
278 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Setup script.
23 | """
24 |
25 | import os
26 |
27 | try:
28 | from setuptools import setup
29 | except ImportError:
30 | from distutils.core import setup
31 |
32 | from quickly import pkginfo
33 |
34 |
35 | def packagelist(directory):
36 | """Return a sorted list with package names for all packages under the given directory."""
37 | folder, basename = os.path.split(directory)
38 | return list(sorted(root[len(folder)+1:].replace(os.sep, '.')
39 | for root, dirs, files in os.walk(directory)
40 | if '__init__.py' in files))
41 |
42 | scripts = []
43 | packages = packagelist('./quickly')
44 | py_modules = []
45 |
46 | with open('README.rst', encoding="utf-8") as f:
47 | long_description = f.read()
48 |
49 | package_data = {
50 |
51 | }
52 |
53 | classifiers = [
54 | 'Development Status :: 3 - Alpha',
55 | 'Intended Audience :: Developers',
56 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
57 | 'Operating System :: MacOS :: MacOS X',
58 | 'Operating System :: Microsoft :: Windows',
59 | 'Operating System :: POSIX',
60 | 'Programming Language :: Python :: 3.6',
61 | 'Topic :: Multimedia :: Sound/Audio',
62 | 'Topic :: Text Processing',
63 | 'Topic :: Text Editors',
64 | ]
65 |
66 | setup(
67 | name = pkginfo.name,
68 | version = pkginfo.version_string,
69 | description = pkginfo.description,
70 | long_description = long_description,
71 | maintainer = pkginfo.maintainer,
72 | maintainer_email = pkginfo.maintainer_email,
73 | url = pkginfo.url,
74 | license = pkginfo.license,
75 |
76 | scripts = scripts,
77 | packages = packages,
78 | package_data = package_data,
79 | py_modules = py_modules,
80 | classifiers = classifiers,
81 | )
82 |
83 |
--------------------------------------------------------------------------------
/tests/test_dom.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test the DOM by comparing manually built tree with parsed trees,
23 | and re-parsing the output of the trees where that makes sense.
24 |
25 | """
26 |
27 | ### find quickly
28 | import sys
29 | sys.path.insert(0, '.')
30 |
31 | import quickly
32 |
33 |
34 | import fractions
35 |
36 | from parce.transform import transform_text
37 | from quickly.lang import html, lilypond, scheme, latex
38 | from quickly.dom import htm, lily, scm, tex, read
39 |
40 |
41 | def check_output(root_lexicon, text, tree):
42 | """Tests whether text->dom and dom->text works properly.
43 |
44 | Transforms text with root_lexicon, then returns True if the created tree
45 | compares equal with the given tree, and a tree transformed from the written
46 | output of the specified tree also compares equal.
47 |
48 | """
49 | tree2 = transform_text(root_lexicon, text)
50 | return tree.equals(tree2) and tree.equals(transform_text(root_lexicon, tree.write()))
51 |
52 |
53 | def check_spanners():
54 | """Test various spanner features."""
55 | # find slur end with spanner id 1
56 | n = read.lily(r"{ c\=1( d e f g\=2) a\=1) }", True)
57 | slur = n.find_descendant(6)
58 | assert slur.find_parallel().pos == 24
59 |
60 | # find slur end with no spanner id
61 | n = read.lily(r"{ c( d e f g\=2) a) }", True)
62 | slur = n.find_descendant(3)
63 | assert slur.find_parallel().pos == 18
64 |
65 | # does not look outside music assignment
66 | n = read.lily_document(r"music = { c\=1( d e f } bmusic= { g\=2) a\=1) }", True)
67 | slur = n.find_descendant(15)
68 | assert slur.find_parallel() is None
69 |
70 |
71 |
72 |
73 | def test_main():
74 |
75 | check_spanners()
76 |
77 | assert check_output(
78 | lilypond.LilyPond.root, "{c4}",
79 | lily.Document(
80 | lily.MusicList(
81 | lily.Note('c',
82 | lily.Duration(
83 | fractions.Fraction(1, 4)))))
84 | )
85 |
86 | assert check_output(
87 | latex.Latex.root, "$x+2$",
88 | tex.Document(tex.MathInlineDollar(tex.Text('x+2')))
89 | )
90 |
91 | assert check_output(
92 | latex.Latex.root, r"\lilypond[staffsize=26]{ { c\breve^\markup \italic { YO! } d'4 } }text.",
93 | tex.Document(
94 | tex.Command('lilypond',
95 | tex.Option(tex.Text('staffsize=26')),
96 | tex.Brace(
97 | lily.Document(
98 | lily.MusicList(
99 | lily.Note('c',
100 | lily.Duration(2),
101 | lily.Articulations(
102 | lily.Direction(1,
103 | lily.Markup(r'markup',
104 | lily.MarkupCommand('italic',
105 | lily.MarkupList(
106 | lily.MarkupWord("YO!"))))))),
107 | lily.Note('d',
108 | lily.Octave(1),
109 | lily.Duration(fractions.Fraction(1, 4))))))),
110 | tex.Text('text.'))
111 | )
112 |
113 | assert check_output(
114 | latex.Latex.root, r"\begin[opts]{lilypond}music = { c }\end{lilypond}",
115 | tex.Document(
116 | tex.Environment(
117 | tex.Command('begin',
118 | tex.Option(tex.Text('opts')),
119 | tex.EnvironmentName('lilypond')),
120 | lily.Document(
121 | lily.Assignment(
122 | lily.Identifier(lily.Symbol('music')),
123 | lily.EqualSign(),
124 | lily.MusicList(
125 | lily.Note('c')))),
126 | tex.Command('end',
127 | tex.EnvironmentName('lilypond'))))
128 | )
129 |
130 | assert check_output(
131 | latex.Latex.root, r"\begin{lilypond}[opts]music = { c }\end{lilypond}",
132 | tex.Document(
133 | tex.Environment(
134 | tex.Command('begin',
135 | tex.EnvironmentName('lilypond'),
136 | tex.Option(tex.Text('opts'))),
137 | lily.Document(
138 | lily.Assignment(
139 | lily.Identifier(lily.Symbol('music')),
140 | lily.EqualSign(),
141 | lily.MusicList(
142 | lily.Note('c')))),
143 | tex.Command('end',
144 | tex.EnvironmentName('lilypond'))))
145 | )
146 |
147 |
148 | if __name__ == "__main__" and 'test_main' in globals():
149 | test_main()
150 |
--------------------------------------------------------------------------------
/tests/test_dom_edit.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test DOM document editing features.
23 | """
24 |
25 | import pytest
26 |
27 | ### find quickly
28 | import sys
29 | sys.path.insert(0, '.')
30 |
31 |
32 |
33 | import parce
34 |
35 | import quickly
36 | from quickly import find
37 | from quickly.dom import lily, util
38 |
39 |
40 |
41 | def test_main():
42 | """Main test function."""
43 |
44 | d = parce.Document(find('lilypond'), "{ c d e f g }", transformer=True)
45 | music = d.get_transform(True)
46 | for note in music // lily.Note('e'):
47 | note.head = 'fis'
48 | assert music.edit(d) == 1
49 | assert d.text() == "{ c d fis f g }"
50 |
51 | d = parce.Document(find('latex'), r"\lilypond{ { c d e f g } }", transformer=True)
52 | music = d.get_transform(True)
53 | for note in music // lily.Note('e'):
54 | note.head = 'fis'
55 | assert music.edit(d) == 1
56 | assert d.text() == r"\lilypond{ { c d fis f g } }"
57 |
58 | d = parce.Document(find('html'), r" { c d e f g } ", transformer=True)
59 | music = d.get_transform(True)
60 | for note in music // lily.Note('e'):
61 | note.head = 'fis'
62 | assert music.edit(d) == 1
63 | assert d.text() == r" { c d fis f g } "
64 |
65 | ### the new Unknown element....
66 | # Now we test the quickly transformer, which handles unknown pieces of text.
67 | d = parce.Document(find('html'),
68 | r' { c d e f g }
', transformer=True)
69 | music = d.get_transform(True)
70 | for note in music // lily.Note('e'):
71 | note.head = 'fis'
72 | assert music.edit(d) == 1
73 | # NOTE we don't loose any text!!
74 | assert d.text() == r' { c d fis f g }
'
75 |
76 | # Again test a document with two music pieces in it:
77 | d = parce.Document(find('html'),
78 | ' { c d e f g }
\n'
79 | ' { a b c d e }
\n',
80 | transformer=True)
81 | music = d.get_transform(True)
82 |
83 | # Now we do not store positions. Bluntly perform the manipulations!
84 | for note in music // lily.Note('e'):
85 | note.head = 'fis'
86 |
87 | # And then bluntly edit the full document. "Unknown" elements will simply be
88 | # skipped, as they are never modified.
89 | assert music.edit(d) == 2
90 | assert d.text() == ' { c d fis f g }
\n' \
91 | + ' { a b c d fis }
\n'
92 |
93 | # writing out should raise a RuntimeError, as Unknown elements do not know
94 | # the text they should print
95 | with pytest.raises(RuntimeError):
96 | music.write()
97 |
98 | # but after replacing the Unknown elements, it should work:
99 | music = d.get_transform(True)
100 | util.replace_unknown(music, d.text())
101 | assert music.write() == '{ c d fis f g }
\n' \
102 | + '{ a b c d fis }
\n'
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | if __name__ == "__main__" and 'test_main' in globals():
111 | test_main()
112 |
--------------------------------------------------------------------------------
/tests/test_dom_util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test the functions in dom.util
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | from quickly.dom import util, read
30 |
31 |
32 |
33 | def test_main():
34 | assert util.whitespace_key('\n\n ') == (2, 2)
35 | assert util.whitespace_key('') == (0, 0)
36 | assert util.whitespace_key(' ') == (0, 1)
37 | assert util.whitespace_key('\n\n') == (2, 0)
38 | assert util.whitespace_key(' ') == (0, 2)
39 |
40 | assert util.combine_text((
41 | ('\n', 'hallo', ' '),
42 | ('\n', 'hallo', ' '),
43 | (' ', 'hallo', ''),
44 | (' ', 'hallo', ''),
45 | ('', 'hallo', '\n'),
46 | )) == ('\n', 'hallo\nhallo hallo hallohallo', '\n')
47 |
48 |
49 | s = r"""(define (attribute-escape s)
50 | (string-substitute "\n" "
"
51 | (string-substitute "\"" """
52 | (string-substitute "&" "&"
53 | s))))
54 | """
55 | d = read.scm(s, True)
56 | util.add_newlines(d, s)
57 | assert d.write_indented() == \
58 | r"""(define (attribute-escape s)
59 | (string-substitute "n" "
"
60 | (string-substitute "\"" """
61 | (string-substitute "&" "&"
62 | s))))
63 | """
64 | assert d.write_indented(max_align_indent=20) == \
65 | r"""(define (attribute-escape s)
66 | (string-substitute "n" "
"
67 | (string-substitute "\"" """
68 | (string-substitute "&" "&"
69 | s))))
70 | """
71 |
72 |
73 |
74 |
75 | if __name__ == "__main__" and 'test_main' in globals():
76 | test_main()
77 |
--------------------------------------------------------------------------------
/tests/test_duration.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test quickly.duration.
23 | """
24 |
25 | from fractions import Fraction
26 |
27 | ### find quickly
28 | import sys
29 | sys.path.insert(0, '.')
30 |
31 |
32 | from quickly.duration import *
33 |
34 |
35 | def test_main():
36 |
37 | assert log_dotcount(7/16) == (2, 2)
38 | assert log_dotcount(Fraction(3, 4)) == (1, 1)
39 |
40 | assert to_string(1) == "1"
41 | assert to_string(1/2) == "2"
42 | assert to_string(1/4) == "4"
43 | assert to_string(3/8) == "4."
44 | assert to_string(7/16) == "4.."
45 |
46 | assert to_string(Fraction(1, 2)) == "2"
47 | assert to_string(Fraction(1, 4)) == "4"
48 | assert to_string(Fraction(3, 8)) == "4."
49 | assert to_string(Fraction(7, 16)) == "4.."
50 |
51 | assert from_string("2...") == Fraction(15, 16)
52 | assert from_string(" \\longa ") == 4
53 | assert from_string("breve") == 2
54 | assert from_string("breve.") == 3
55 | assert from_string("breve..") == Fraction(7, 2)
56 | assert from_string("breve...") == Fraction(15, 4)
57 | assert from_string("4") == Fraction(1, 4)
58 | assert from_string("8") == Fraction(1, 8)
59 | assert from_string("16") == Fraction(1, 16)
60 | assert from_string("32") == Fraction(1, 32)
61 | assert from_string("64") == Fraction(1, 64)
62 | assert from_string("64.") == Fraction(3, 128)
63 |
64 | assert shift_duration(Fraction(1, 8), -1) == Fraction(1, 4)
65 | assert to_string(shift_duration(from_string('2'), 1, 1)) == '4.'
66 |
67 |
68 | if __name__ == "__main__" and 'test_main' in globals():
69 | test_main()
70 |
--------------------------------------------------------------------------------
/tests/test_indent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test the indenting features
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | from quickly.dom import lily, scm, read, indent
30 |
31 |
32 |
33 |
34 |
35 | def test_main():
36 |
37 | d = read.lily_document("{ { c d e f g } }")
38 | assert d.write_indented() == '{ { c d e f g } }\n'
39 |
40 | next(d // lily.Note('d')).space_after = '\n'
41 | assert d.write_indented() == '''\
42 | { { c d
43 | e f g } }
44 | '''
45 |
46 | d = read.scm_document("(if a b c)")
47 | next(d // scm.Identifier('a')).space_after = '\n'
48 | assert d.write_indented() == '''\
49 | (if a
50 | b c)
51 | '''
52 |
53 | d = read.scm_document("((if a b c))")
54 | next(d // scm.Identifier('a')).space_after = '\n'
55 | assert d.write_indented() == '''\
56 | ((if a
57 | b c))
58 | '''
59 |
60 | d = read.scm_document('((string-append "een" "twee" "drie"))')
61 | for n in d[0][0][2:]:
62 | n.space_before = '\n' # twee and drie on new line
63 | assert d.write_indented() == '''\
64 | ((string-append "een"
65 | "twee"
66 | "drie"))
67 | '''
68 |
69 | d = read.scm_document('((blaat) (string-append "een" "twee" "drie"))')
70 | for n in d[0][1][2:]:
71 | n.space_before = '\n' # twee and drie on new line
72 | assert d.write_indented() == '''\
73 | ((blaat) (string-append "een"
74 | "twee"
75 | "drie"))
76 | '''
77 |
78 | assert d.write_indented(max_align_indent=24) == '''\
79 | ((blaat) (string-append "een"
80 | "twee"
81 | "drie"))
82 | '''
83 |
84 |
85 |
86 | if __name__ == "__main__" and 'test_main' in globals():
87 | test_main()
88 |
--------------------------------------------------------------------------------
/tests/test_key.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2022 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test the key module
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | import quickly
30 | from quickly.key import *
31 | from quickly.pitch import Pitch
32 |
33 |
34 | def test_main():
35 | """Main test function."""
36 | sig = KeySignature(0, 0, "major")
37 | assert sig.pitch(61) == Pitch(0, 0, 0.5)
38 |
39 |
40 |
41 |
42 |
43 | if __name__ == "__main__" and 'test_main' in globals():
44 | test_main()
45 |
--------------------------------------------------------------------------------
/tests/test_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test the node module.
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | from quickly.node import Node
30 |
31 |
32 | class N1(Node):
33 | pass
34 |
35 |
36 | class N2(Node):
37 | pass
38 |
39 |
40 | class N3(Node):
41 | pass
42 |
43 |
44 | class M1(N1):
45 | pass
46 |
47 |
48 | class M2(N2):
49 | pass
50 |
51 |
52 | class M3(N3):
53 | pass
54 |
55 |
56 | tree = \
57 | N1(
58 | N2(
59 | N3(),
60 | M3(),
61 | N2(),
62 | M1(),
63 | ),
64 | N1(
65 | M2(),
66 | ),
67 | )
68 |
69 |
70 | def test_main():
71 | assert next(tree//M3) is tree[0][1]
72 | assert len(list(tree/N2)) == 1
73 | assert sum(1 for _ in tree//N2) == 3 # M2 inherits from N2 :-)
74 | assert sum(1 for _ in tree.instances_of(N2)) == 2 # topmost N2's children are skipped
75 | assert next(tree[1][0] << N1) is tree[1]
76 | tree2 = tree.copy()
77 | assert tree.equals(tree2)
78 | tree2[0][3] = N1()
79 | assert not tree.equals(tree2)
80 | assert tree[0][2].common_ancestor(tree[1][0]) is tree
81 | assert tree[0][2].common_ancestor(tree2[1][0]) is None
82 |
83 |
84 |
85 | if __name__ == "__main__" and 'test_main' in globals():
86 | test_main()
87 |
88 |
--------------------------------------------------------------------------------
/tests/test_numbering.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test quickly.numbering
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | import quickly
30 |
31 |
32 |
33 |
34 |
35 | def test_main():
36 |
37 | from quickly.numbering import (
38 | int2roman, int2text, int2letter, letter2int, roman2int, text2int)
39 |
40 | assert int2roman(2021) == "MMXXI"
41 | assert int2roman(1967) == "MCMLXVII"
42 |
43 | for val in range(1, 9999):
44 | assert roman2int(int2roman(val)) == val
45 |
46 | assert int2text(12345) == "TwelveThousandThreeHundredFortyFive"
47 | assert text2int("ThousandTwoHundred") == 1200
48 |
49 | for val in range(9999):
50 | assert text2int(int2text(val)) == val
51 |
52 | assert int2letter(27) == "AA"
53 | assert letter2int("AB") == 28
54 |
55 | for val in range(9999):
56 | assert letter2int(int2letter(val)) == val
57 |
58 |
59 |
60 | if __name__ == "__main__" and 'test_main' in globals():
61 | test_main()
62 |
--------------------------------------------------------------------------------
/tests/test_pitch.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test pitch related functions (pitch, transpose, relative, ...)
23 | """
24 |
25 | import pytest
26 |
27 | ### find quickly
28 | import sys
29 | sys.path.insert(0, '.')
30 |
31 | import parce
32 |
33 | import quickly
34 | from quickly.pitch import (
35 | Pitch, PitchProcessor, octave_to_string, octave_from_string, determine_language)
36 | from quickly.transpose import Transposer, Transpose
37 | from quickly.relative import Abs2rel, Rel2abs, abs2rel, rel2abs
38 | from quickly.dom import read, lily
39 | from quickly import find
40 |
41 |
42 | def lydoc(text):
43 | """Return a parce LilyPond doc with text."""
44 | return parce.Document(find('lilypond'), text, transformer=True)
45 |
46 |
47 | def check_pitch():
48 | """Test pitch manipulations."""
49 | p = PitchProcessor()
50 | c = Pitch(-1, 0, 0)
51 | cis = Pitch(-1, 0, 0.5)
52 | d = Pitch(-1, 1, 0)
53 | dis = Pitch(-1, 1, 0.5)
54 | disis = Pitch(-1, 1, 1)
55 |
56 | assert 'c' == p.to_string(c)
57 | assert 'cis' == p.to_string(cis)
58 |
59 | p.language = 'english'
60 | assert 'cs' == p.to_string(cis)
61 | p.prefer_long = True
62 | assert 'c-sharp' == p.to_string(cis)
63 |
64 | p.language = 'français'
65 | p.prefer_accented = True
66 | assert 'ré' == p.to_string(d)
67 |
68 | p.prefer_x = True
69 | assert 'réx' == p.to_string(disis)
70 | p.prefer_x = False
71 | assert 'rédd' == p.to_string(disis)
72 | p.prefer_accented = False
73 | p.prefer_x = True
74 | assert 'rex' == p.to_string(disis)
75 | p.prefer_x = False
76 | assert 'redd' == p.to_string(disis)
77 |
78 | with pytest.raises(KeyError):
79 | p.language = "does_not_exist"
80 |
81 | p.language = "english"
82 | assert 'c-sharp' == p.to_string(cis)
83 |
84 | del p.language
85 | assert p.language == "nederlands"
86 | assert p.pitch('dis') == dis
87 |
88 | p.language = 'français'
89 | assert p.pitch('réx') == disis
90 | assert p.pitch('rex') == disis
91 | assert p.pitch('rédd') == disis
92 | assert p.pitch('redd') == disis
93 |
94 | # distill prefs
95 | p = PitchProcessor()
96 | p.distill_preferences(['es', 'g'])
97 | assert p.prefer_classic == True
98 | p.distill_preferences(['ees', 'g'])
99 | assert p.prefer_classic == False
100 |
101 | # a non-specific note does not change the pref
102 | p.distill_preferences(['fis', 'g'])
103 | assert p.prefer_classic == False
104 | p.prefer_classic = True
105 | p.distill_preferences(['fis', 'g'])
106 | assert p.prefer_classic == True
107 |
108 | # one note changes two prefs
109 | p = PitchProcessor("français")
110 | p.prefer_x = False
111 | p.prefer_accented = False
112 | p.distill_preferences(['réx'])
113 | assert p.prefer_x == True
114 | assert p.prefer_accented == True
115 |
116 | # unaccented pref set back
117 | p.distill_preferences(['red'])
118 | assert p.prefer_x == True
119 | assert p.prefer_accented == False
120 |
121 | # x pref set back
122 | p.distill_preferences(['redd'])
123 | assert p.prefer_x == False
124 | assert p.prefer_accented == False
125 |
126 | # node editing
127 | pp = PitchProcessor()
128 | n = lily.Note('c', octave=2)
129 | with pp.process(n) as p:
130 | p.note += 2
131 | p.octave = 0
132 | assert n.head == 'e'
133 | assert n.octave == 1
134 |
135 | # Pitch class
136 | assert Pitch(0, 0, 0) < Pitch(0, 0, 1)
137 | assert Pitch(0, 0, 0) > Pitch(-1, 1, 0)
138 | assert Pitch(0, 1, .25) == Pitch(0, 1, .25)
139 |
140 |
141 | # other functions
142 | assert octave_to_string(3) == "'''"
143 | assert octave_to_string(-3) == ",,,"
144 | assert octave_from_string(",") == -1
145 | assert octave_from_string("''") == 2
146 | assert octave_from_string("',") == 0
147 |
148 | assert list(determine_language(['c', 'd', 'e', 'f', 'g'])) == \
149 | ['nederlands', 'english', 'deutsch', 'norsk', 'suomi', 'svenska', 'arabic', 'bagpipe', 'persian']
150 | assert list(determine_language(['c', 'd', 'es', 'f', 'g'])) == \
151 | ['nederlands', 'english', 'deutsch', 'norsk', 'suomi', 'persian']
152 | assert list(determine_language(['c', 'd', 'es', 'fis', 'g', 'bis'])) == \
153 | ['nederlands']
154 | assert list(determine_language(['do', 'ré', 'r'])) == \
155 | ['français'] # r is ignored, ré with accent is français
156 |
157 |
158 | def check_transpose():
159 | """Test Transposer."""
160 | t = Transposer(Pitch(0, 0, 0), Pitch(0, 2, 0))
161 | p = Pitch(0, 0, 0)
162 | t.transpose(p)
163 | assert p == Pitch(0, 2, 0)
164 |
165 | p = Pitch(0, 6, 0)
166 | t.transpose(p)
167 | assert p == Pitch(1, 1, 0.5)
168 |
169 | t = Transposer(Pitch(0, 0, 0), Pitch(0, 2, 0))
170 | music = read.lily_document("{ c d e f g }")
171 | Transpose(t).edit_node(music)
172 | assert music.write() == "{ e fis gis a b }"
173 | Transpose(t).edit_node(music)
174 | assert music.write() == "{ gis ais bis cis' dis' }"
175 |
176 | t = Transposer(Pitch(0, 0, 0), Pitch(1, 0, 0))
177 | music = read.lily_document(r"\relative { c' d e f g }")
178 | Transpose(t).edit_node(music)
179 | assert music.write() == r"\relative { c'' d e f g }"
180 |
181 | t = Transposer(Pitch(0, 0, 0), Pitch(0, 4, 0))
182 | music = read.lily_document(r"\relative c' { c d e f g }")
183 | Transpose(t).edit_node(music)
184 | assert music.write() == r"\relative g' { g a b c d }"
185 |
186 | t = Transposer(Pitch(0, 0, 0), Pitch(0, 1, 0))
187 | music = read.lily_document(r"\relative { c' d' e, f g }")
188 | Transpose(t).edit_node(music)
189 | assert music.write() == r"\relative { d' e' fis, g a }"
190 |
191 | t = Transposer(Pitch(0, 0, 0), Pitch(-1, 6, .5))
192 | music = read.lily_document(r"\relative { c' d' e, f g }")
193 | Transpose(t, relative_first_pitch_absolute=True).edit_node(music)
194 | assert music.write() == r"\relative { bis cisis' disis, eis fisis }"
195 |
196 | t = Transposer(Pitch(0, 0, 0), Pitch(-1, 6, .5))
197 | music = read.lily_document(r"\relative c' { c d' e, f g }")
198 | Transpose(t, relative_first_pitch_absolute=True).edit_node(music)
199 | assert music.write() == r"\relative bis { bis cisis' disis, eis fisis }"
200 |
201 | t = Transposer(Pitch(0, 0, 0), Pitch(-1, 6, -.5))
202 | music = read.lily_document(r"\relative { g a b c d }")
203 | Transpose(t, relative_first_pitch_absolute=False).edit_node(music)
204 | assert music.write() == r"\relative { f, g a bes c }"
205 |
206 | music = read.lily_document(r"\relative { g a b c d }")
207 | Transpose(t, relative_first_pitch_absolute=True).edit_node(music)
208 | assert music.write() == r"\relative { f g a bes c }"
209 |
210 | music = read.lily_document("""\\version "2.12.0"\n\\relative { g a b c d }\n""")
211 | Transpose(t).edit_node(music)
212 | assert music[1].write() == r"\relative { f, g a bes c }"
213 |
214 | music = read.lily_document("""\\version "2.22.0"\n\\relative { g a b c d }\n""")
215 | Transpose(t).edit_node(music)
216 | assert music[1].write() == r"\relative { f g a bes c }"
217 |
218 | doc = lydoc("{ c d e f g }")
219 | cur = parce.Cursor(doc).select(4, 7)
220 | t = Transposer(Pitch(0, 0, 0), Pitch(0, 2, 0))
221 | Transpose(t).edit_cursor(cur)
222 | assert doc.text() == "{ c fis gis f g }" # only two notes changed
223 |
224 | # chordmode, inversion should never get an octave
225 | doc = lydoc(r"\chords { c/g }")
226 | t = Transposer(Pitch(0, 0, 0), Pitch(-1, 0, 0))
227 | Transpose(t).edit(doc)
228 | assert doc.text() == r"\chords { c,/g }"
229 |
230 | doc = lydoc(r"\relative c' \chordmode { g:7 | c2/e }")
231 | t = Transposer(Pitch(0, 0, 0), Pitch(-1, 0, 0))
232 | Transpose(t).edit(doc)
233 | assert doc.text() == r"\relative c \chordmode { g:7 | c2/e }"
234 |
235 |
236 | def check_relative():
237 | """Test functions in the relative module."""
238 | # abs2rel
239 | doc = lydoc("{ c' d' e' f' g' }")
240 | abs2rel(doc)
241 | assert doc.text() == r"\relative c' { c d e f g }"
242 |
243 | doc = lydoc("{ c' d' e' f' g' }")
244 | Abs2rel(start_pitch=False).edit(doc)
245 | assert doc.text() == r"\relative { c d e f g }"
246 |
247 | doc = lydoc("{ c' d' e' f' g' }")
248 | Abs2rel(start_pitch=False, first_pitch_absolute=True).edit(doc)
249 | assert doc.text() == r"\relative { c' d e f g }"
250 |
251 | doc = lydoc("{ { c' d' e' f' g' } { d' e' fis' g' a' } }")
252 | Abs2rel(start_pitch=False, first_pitch_absolute=True).edit(doc)
253 | assert doc.text() == r"{ \relative { c' d e f g } \relative { d' e fis g a } }"
254 |
255 | doc = lydoc("{ c { c' d' e' f' g' } { d' e' fis' g' a' } }")
256 | cur = parce.Cursor(doc)
257 | Abs2rel(start_pitch=False, first_pitch_absolute=True).edit(cur)
258 | assert doc.text() == r"\relative { c { c' d e f g } { d e fis g a } }"
259 |
260 | doc = lydoc("{ c { c' d e' f g' } { d' e'' fis' g a' } }")
261 | cur = parce.Cursor(doc)
262 | Abs2rel(start_pitch=False, first_pitch_absolute=True).edit(cur)
263 | assert doc.text() == r"\relative { c { c' d, e' f, g' } { d e' fis, g, a' } }"
264 |
265 | # rel2abs
266 | doc = lydoc(r"music = \relative c' { c d e f g }")
267 | rel2abs(doc)
268 | assert doc.text() == "music = { c' d' e' f' g' }"
269 | abs2rel(doc)
270 | assert doc.text() == r"music = \relative c' { c d e f g }"
271 |
272 | # chordmode
273 | doc = lydoc(r"music = \relative c' \chordmode { c/g d e f g }")
274 | rel2abs(doc)
275 | assert doc.text() == r"music = \chordmode { c'/g d' e' f' g' }"
276 | abs2rel(doc)
277 | assert doc.text() == r"music = \relative c' \chordmode { c/g d e f g }"
278 |
279 |
280 | def test_main():
281 | """Main test function."""
282 | check_pitch()
283 | check_transpose()
284 | check_relative()
285 |
286 |
287 | if __name__ == "__main__" and 'test_main' in globals():
288 | test_main()
289 |
--------------------------------------------------------------------------------
/tests/test_rhythm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Tests for rhythm module.
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | from fractions import Fraction
30 |
31 | import parce
32 |
33 | import quickly
34 | from quickly.rhythm import *
35 | from quickly.dom import read
36 |
37 |
38 | def test_main():
39 | """Main test function."""
40 |
41 | m = read.lily_document(r"{ c4 d e f g }")
42 | assert copy(m) == [(Fraction(1, 4), 1), None, None, None, None]
43 |
44 | paste(m, [(Fraction(1, 2), 1/2), None, None])
45 | assert m.write() == r"{ c2*1/2 d e f2*1/2 g }"
46 |
47 | # \skip does not change "previous" duration
48 | m = read.lily_document(r"{ c4 d e \skip 2 f g }")
49 | explicit(m)
50 | assert m.write() == r"{ c4 d4 e4 \skip 2 f4 g4 }"
51 |
52 | # \skip duration may not be removed
53 | remove(m)
54 | assert m.write() == r"{ c d e \skip 2 f g }"
55 |
56 | # duration may not be removed when immediate next is an Unpitched
57 | m = read.lily_document(r"{ c4 d8 8 8 g }")
58 | remove(m)
59 | assert m.write() == r"{ c d8 8 8 g }"
60 |
61 | # but when there is an articulation it is no problem:
62 | m = read.lily_document(r"{ c4 d8-- 8 8 g }")
63 | remove(m)
64 | assert m.write() == r"{ c d-- 8 8 g }"
65 |
66 | # both with paste None, result should be same
67 | m = read.lily_document(r"{ c4 d8 8 8 g }")
68 | paste(m, [None])
69 | assert m.write() == r"{ c d8 8 8 g }"
70 | m = read.lily_document(r"{ c4 d8-- 8 8 g }")
71 | paste(m, [None])
72 | assert m.write() == r"{ c d-- 8 8 g }"
73 |
74 | # lyrics
75 | m = read.lily_document(r"""\lyricmode { hoi4 \markup hoi "wil" -- bert }""")
76 | explicit(m)
77 | assert m.write() == r"""\lyricmode { hoi4 \markup hoi 4 "wil"4 -- bert4 }"""
78 |
79 | # implicit per line
80 | d = parce.Document(quickly.find('lilypond'), r'''
81 | music = {
82 | c4 d8 e8 f8 g8 a4
83 | g f e4 d
84 | c d4 e2
85 | }
86 | ''', transformer=True)
87 | implicit(d, True)
88 | assert d.text() == r'''
89 | music = {
90 | c4 d8 e f g a4
91 | g4 f e d
92 | c4 d e2
93 | }
94 | '''
95 |
96 | # implicit per line, with unpitched that may not be removed
97 | d = parce.Document(quickly.find('lilypond'), r'''
98 | music = {
99 | c4 d8 e8 f8 g8 a4
100 | g f e4 d
101 | c d4 4 4
102 | }
103 | ''', transformer=True)
104 | implicit(d, True)
105 | assert d.text() == r'''
106 | music = {
107 | c4 d8 e f g a4
108 | g4 f e d
109 | c4 d4 4 4
110 | }
111 | '''
112 |
113 |
114 |
115 | if __name__ == "__main__" and 'test_main' in globals():
116 | test_main()
117 |
--------------------------------------------------------------------------------
/tests/test_scheme.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2020 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Test Scheme transform.
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | from quickly.dom import scm
30 |
31 | from quickly.lang.scheme import Scheme
32 | from parce.transform import transform_text
33 |
34 |
35 | scheme_doc = """
36 | ; some constructs
37 | (define var 'symbol)
38 | (define (name args) (body))
39 |
40 | ; a list
41 | (1 2 3 4 5)
42 |
43 | ; a partially quoted list
44 | `(a b c ,@(d e f) g)
45 |
46 | ; a string
47 | ("a string")
48 |
49 | ; a hex value with fraction :-)
50 | #xdead/beef
51 |
52 | ; same value in decimal
53 | 57005/48879
54 | """
55 |
56 | def test_main():
57 | d = transform_text(Scheme.root, scheme_doc)
58 | assert len(d) == 13
59 | assert sum(1 for _ in d//scm.Number) == 7
60 | assert sum(1 for _ in d//scm.String) == 1
61 | assert sum(1 for _ in d//scm.Identifier) == 14
62 |
63 | # the two fractions
64 | assert d[10].head == d[12].head
65 |
66 | # does find_descendant work propery?
67 | assert d.find_descendant(40).head == "("
68 | assert d.find_descendant(41).head == "define"
69 | l = d.find_descendant(59)
70 | assert isinstance(l, scm.List)
71 | assert l.pos == 48
72 |
73 | # see if the output is correct, and when transformed again as well...
74 | output = d.write()
75 | d1 = transform_text(Scheme.root, output)
76 | assert d.equals(d1)
77 | assert output == d1.write()
78 |
79 |
80 |
81 |
82 | if __name__ == "__main__" and 'test_main' in globals():
83 | test_main()
84 |
--------------------------------------------------------------------------------
/tests/test_template.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2022 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Template for test files that test quickly.
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 | import quickly
30 |
31 |
32 |
33 |
34 | def test_main():
35 | """Main test function."""
36 |
37 |
38 |
39 |
40 |
41 | if __name__ == "__main__" and 'test_main' in globals():
42 | test_main()
43 |
--------------------------------------------------------------------------------
/tests/test_time.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file is part of `quickly`, a library for LilyPond and the `.ly` format
4 | #
5 | # Copyright © 2019-2022 by Wilbert Berendsen
6 | #
7 | # This module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 |
20 |
21 | """
22 | Template for test files that test quickly.
23 | """
24 |
25 | ### find quickly
26 | import sys
27 | sys.path.insert(0, '.')
28 |
29 |
30 | from fractions import Fraction
31 |
32 | import parce
33 | import quickly
34 | from quickly.time import Time
35 | from quickly.dom import lily, read
36 |
37 |
38 |
39 | def test_main():
40 | """Main test function."""
41 | d = parce.Document(quickly.find('lilypond'), r'''
42 | music = { c4 d e f }
43 |
44 | { c2 \music g a b8 g f d }
45 | ''', transformer=True)
46 |
47 | m = d.get_transform(True)
48 | t = Time()
49 | assert t.duration(m[1][0], m[1][2]).time == 2 # length of "c2 \music g" part (g has duration 2)
50 |
51 | c = parce.Cursor(d, 44, 47)
52 | assert t.cursor_position(c).time == Fraction(11, 4) # length or music before the cursor
53 | assert t.cursor_duration(c).time == Fraction(1, 4) # duration of the selected music
54 |
55 | m = read.lily(r"\tuplet 3/2 { c8 d e }")
56 | assert t.length(m) == Fraction(1, 4)
57 | assert t.length(m[1][1]) == Fraction(1, 12) # one note in tuplet
58 |
59 | m = read.lily(r"\shiftDurations #1 #1 { c4 d e f }")
60 | assert t.length(m) == Fraction(3, 4) # note value halved and dot added, so should be 3/4
61 | assert t.length(m[2][2]) == Fraction(3, 16) # autodiscovers the current duration transform
62 |
63 |
64 |
65 | if __name__ == "__main__" and 'test_main' in globals():
66 | test_main()
67 |
--------------------------------------------------------------------------------