├── .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 | --------------------------------------------------------------------------------