├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── mau ├── __init__.py ├── environment │ ├── __init__.py │ └── environment.py ├── errors.py ├── helpers.py ├── lexers │ ├── __init__.py │ ├── arguments_lexer.py │ ├── base_lexer.py │ ├── main_lexer.py │ ├── preprocess_variables_lexer.py │ └── text_lexer.py ├── main.py ├── nodes │ ├── __init__.py │ ├── arguments.py │ ├── block.py │ ├── content.py │ ├── footnotes.py │ ├── header.py │ ├── inline.py │ ├── lists.py │ ├── macros.py │ ├── nodes.py │ ├── page.py │ ├── paragraph.py │ ├── source.py │ └── toc.py ├── parsers │ ├── __init__.py │ ├── arguments.py │ ├── arguments_parser.py │ ├── attributes.py │ ├── base_parser.py │ ├── footnotes.py │ ├── internal_links.py │ ├── main_parser.py │ ├── preprocess_variables_parser.py │ ├── text_parser.py │ └── toc.py ├── test_helpers.py ├── text_buffer │ ├── __init__.py │ ├── context.py │ └── text_buffer.py ├── tokens │ ├── __init__.py │ └── tokens.py └── visitors │ ├── __init__.py │ ├── base_visitor.py │ └── jinja_visitor.py ├── noxfile.py ├── pyproject.toml ├── requirements.txt ├── requirements ├── development.txt ├── production.txt └── testing.txt └── tests ├── __init__.py ├── e2e ├── __init__.py ├── expected │ ├── block │ │ ├── block_admonition.yaml │ │ ├── block_conditionals.yaml │ │ ├── block_quote.yaml │ │ ├── block_simple.yaml │ │ └── block_title.yaml │ ├── commands │ │ ├── footnotes.yaml │ │ ├── footnotes_advanced.yaml │ │ ├── references.yaml │ │ ├── toc.yaml │ │ └── toc_advanced.yaml │ ├── inline │ │ ├── classes.yaml │ │ ├── conditionals.yaml │ │ ├── inline_image.yaml │ │ ├── internal_link.yaml │ │ ├── link.yaml │ │ ├── mailto.yaml │ │ ├── multiple_paragraphs.yaml │ │ ├── single_paragraph.yaml │ │ ├── split_paragraph.yaml │ │ ├── styles.yaml │ │ ├── styles_mixed.yaml │ │ └── verbatim.yaml │ ├── other │ │ ├── directives.yaml │ │ ├── variable_advanced.yaml │ │ └── variable_simple.yaml │ ├── page │ │ ├── comments.yaml │ │ ├── header_multiple.yaml │ │ ├── header_single.yaml │ │ ├── header_with_paragraphs.yaml │ │ ├── horizontal_line.yaml │ │ ├── image.yaml │ │ ├── list_ordered.yaml │ │ ├── list_start.yaml │ │ └── list_unordered.yaml │ └── source │ │ ├── source_callouts.yaml │ │ ├── source_highlight.yaml │ │ └── source_simple.yaml ├── source │ ├── block │ │ ├── block_admonition.mau │ │ ├── block_conditionals.mau │ │ ├── block_quote.mau │ │ ├── block_simple.mau │ │ └── block_title.mau │ ├── commands │ │ ├── footnotes.mau │ │ ├── footnotes_advanced.mau │ │ ├── toc.mau │ │ └── toc_advanced.mau │ ├── inline │ │ ├── classes.mau │ │ ├── conditionals.mau │ │ ├── inline_image.mau │ │ ├── internal_link.mau │ │ ├── link.mau │ │ ├── mailto.mau │ │ ├── multiple_paragraphs.mau │ │ ├── single_paragraph.mau │ │ ├── split_paragraph.mau │ │ ├── styles.mau │ │ ├── styles_mixed.mau │ │ └── verbatim.mau │ ├── other │ │ ├── directives.mau │ │ ├── include_text.txt │ │ ├── variable_advanced.mau │ │ └── variable_simple.mau │ ├── page │ │ ├── comments.mau │ │ ├── header_multiple.mau │ │ ├── header_single.mau │ │ ├── header_with_paragraphs.mau │ │ ├── horizontal_line.mau │ │ ├── image.mau │ │ ├── list_ordered.mau │ │ ├── list_start.mau │ │ └── list_unordered.mau │ └── source │ │ ├── source_callouts.mau │ │ ├── source_highlight.mau │ │ └── source_simple.mau └── test_e2e.py ├── environment └── test_environment.py ├── helpers.py ├── lexers ├── __init__.py ├── data_file_lexer.txt ├── test_arguments_lexer.py ├── test_base_lexer.py ├── test_main_lexer.py ├── test_preprocess_variables_lexer.py └── test_text_lexer.py ├── nodes ├── __init__.py ├── test_arguments.py ├── test_block.py ├── test_content.py ├── test_footnotes.py ├── test_header.py ├── test_inline.py ├── test_lists.py ├── test_macros.py ├── test_nodes.py ├── test_page.py ├── test_paragraph.py ├── test_references.py ├── test_source.py └── test_toc.py ├── parsers ├── __init__.py ├── main_parser │ ├── __init__.py │ ├── test_arguments.py │ ├── test_block_attributes.py │ ├── test_block_basic.py │ ├── test_block_engine_group.py │ ├── test_block_engine_mau.py │ ├── test_block_engine_raw.py │ ├── test_block_other.py │ ├── test_block_source.py │ ├── test_content.py │ ├── test_control.py │ ├── test_footnotes.py │ ├── test_headers.py │ ├── test_horizontal_rule.py │ ├── test_internal_links.py │ ├── test_lists.py │ ├── test_main_parser.py │ ├── test_paragraph.py │ ├── test_parent.py │ ├── test_source.py │ ├── test_title.py │ ├── test_toc.py │ └── test_variables.py ├── test_arguments_parser.py ├── test_base_parser.py ├── test_create_toc.py ├── test_preprocess_variables_parser.py └── text_parser │ ├── __init__.py │ ├── macros │ ├── __init__.py │ ├── test_arguments.py │ ├── test_class.py │ ├── test_control.py │ ├── test_footnote.py │ ├── test_generic_macro.py │ ├── test_header.py │ ├── test_image.py │ └── test_link.py │ ├── test_basic_text.py │ ├── test_escaped.py │ ├── test_style.py │ └── test_verbatim.py ├── text_buffer ├── __init__.py ├── test_context.py └── test_text_buffer.py ├── tokens ├── __init__.py └── test_tokens.py └── visitors ├── __init__.py ├── base_visitor ├── __init__.py ├── test_block.py ├── test_content.py ├── test_footnotes.py ├── test_header.py ├── test_inline.py ├── test_lists.py ├── test_page.py ├── test_paragraph.py ├── test_source.py └── test_toc.py └── jinja_visitor ├── test_block.py ├── test_content.py ├── test_footnotes.py ├── test_header.py ├── test_inline.py ├── test_lists.py ├── test_page.py ├── test_paragraph.py ├── test_source.py ├── test_templates.py └── toc.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !.isort.cfg 7 | !setup.cfg 8 | *.orig 9 | *.log 10 | *.pot 11 | __pycache__/* 12 | .cache/* 13 | .*.swp 14 | */.ipynb_checkpoints/* 15 | .DS_Store 16 | 17 | # Project files 18 | .ropeproject 19 | .project 20 | .pydevproject 21 | .settings 22 | .idea 23 | .vscode 24 | tags 25 | 26 | # Package files 27 | *.egg 28 | *.eggs/ 29 | .installed.cfg 30 | *.egg-info 31 | 32 | # Unittest and coverage 33 | htmlcov/* 34 | .coverage 35 | .tox 36 | junit.xml 37 | coverage.xml 38 | .pytest_cache/ 39 | 40 | # Build and docs folder/files 41 | build/* 42 | dist/* 43 | sdist/* 44 | docs/api/* 45 | docs/_rst/* 46 | docs/_build/* 47 | cover/* 48 | MANIFEST 49 | 50 | # Per-project virtualenvs 51 | .venv*/ 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Leonardo Giordani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mau v4 2 | 3 | Mau is a lightweight markup language heavily inspired by [AsciiDoc](https://asciidoctor.org/docs/what-is-asciidoc), [Asciidoctor](https://asciidoctor.org/) and [Markdown](https://daringfireball.net/projects/markdown/). 4 | 5 | As I wasn't satisfied by the results I got with those tools I decided to try to write my own language and the tool to render it. 6 | 7 | I am currently using Mau to render posts on my blog [The Digital Cat](https://www.thedigitalcatonline.com) and the online version of my book ["Clean Architectures in Python"](https://www.thedigitalcatbooks.com). I also used it to transpile the code of the book to Markua, to be able to publish the book on Leanpub using their toolchain. 8 | 9 | ## Quick start 10 | 11 | To install Mau use `pip` 12 | 13 | ``` sh 14 | pip install mau 15 | ``` 16 | 17 | Then install at least one visitor plugin. You probably want to start with `mau-html-visitor` 18 | 19 | ``` sh 20 | pip install mau-html-visitor 21 | ``` 22 | 23 | To convert Mau sources into HTML just run 24 | 25 | ``` sh 26 | mau -i source.mau -o destination.html -f html 27 | ``` 28 | 29 | Check out Mau [documentation](https://project-mau.github.io/) for further information. 30 | 31 | ## Pelican plugin 32 | 33 | There is a Pelican plugin that enables you to use Mau in your blog. Check it at https://github.com/pelican-plugins/mau-reader. 34 | 35 | ## Support 36 | 37 | You may report bugs or missing features use the [issues page](https://github.com/Project-Mau/mau/issues). 38 | If you want to ask for help or discuss ideas use the [discussions page](https://github.com/Project-Mau/mau/discussions) 39 | -------------------------------------------------------------------------------- /mau/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-import 2 | 3 | from mau.environment.environment import Environment 4 | from mau.lexers.main_lexer import MainLexer 5 | from mau.lexers.text_lexer import TextLexer 6 | from mau.parsers.main_parser import MainParser 7 | from mau.text_buffer.context import Context 8 | from mau.text_buffer.text_buffer import TextBuffer 9 | from mau.visitors.base_visitor import BaseVisitor 10 | 11 | 12 | # pylint: disable=import-outside-toplevel 13 | def load_visitors(): 14 | """ 15 | This function loads all the visitors belonging to 16 | the group "mau.visitors". This code has been isolated 17 | in a function to allow visitor modules to import the 18 | Mau package without creating a cycle. 19 | """ 20 | 21 | import sys 22 | 23 | if sys.version_info < (3, 10): 24 | from importlib_metadata import entry_points 25 | else: 26 | from importlib.metadata import entry_points 27 | 28 | discovered_plugins = entry_points(group="mau.visitors") 29 | 30 | # Load the available visitors 31 | visitors = [i.load() for i in discovered_plugins] 32 | visitors.append(BaseVisitor) 33 | 34 | return visitors 35 | 36 | 37 | class ConfigurationError(ValueError): 38 | """Used to signal an error in the configuration""" 39 | 40 | 41 | class Mau: 42 | def __init__( 43 | self, 44 | input_file_name, 45 | environment=None, 46 | ): 47 | # This is needed to set up the initial context for the lexer 48 | self.input_file_name = input_file_name 49 | 50 | # This will contain all the variables declared 51 | # in the text and in the configuration 52 | self.environment = environment or Environment() 53 | 54 | self.lexer = MainLexer(self.environment) 55 | self.parser = MainParser(self.environment) 56 | 57 | def run_lexer(self, text): 58 | context = Context(source=self.input_file_name) 59 | text_buffer = TextBuffer(text, context) 60 | 61 | self.lexer.process(text_buffer) 62 | 63 | def run_parser(self, tokens): 64 | self.parser.parse(tokens) 65 | self.parser.finalise() 66 | 67 | def create_visitor(self): 68 | visitor_class = self.environment.getvar("mau.visitor.class") 69 | 70 | return visitor_class( 71 | environment=self.environment, 72 | ) 73 | 74 | def run_visitor(self, node): 75 | visitor = self.create_visitor() 76 | 77 | # Visit the document AST 78 | return visitor.visit(node) 79 | -------------------------------------------------------------------------------- /mau/environment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/environment/__init__.py -------------------------------------------------------------------------------- /mau/environment/environment.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from collections.abc import MutableMapping 3 | 4 | 5 | class EnvironmentABC(ABC): 6 | pass 7 | 8 | 9 | def flatten_nested_dict(nested, parent_key=None, separator="."): 10 | flat = {} 11 | 12 | for key, value in nested.items(): 13 | key = f"{parent_key}{separator}{key}" if parent_key else key 14 | 15 | if isinstance(value, MutableMapping) and len(value) != 0: 16 | flat.update(flatten_nested_dict(value, key, separator=separator)) 17 | else: 18 | flat[key] = value 19 | 20 | return flat 21 | 22 | 23 | def nest_flattened_dict(flat, separator="."): 24 | def _split(key, value, separator, output): 25 | key, *rest = key.split(separator, 1) 26 | 27 | if rest: 28 | _split( 29 | rest[0], 30 | value, 31 | separator, 32 | output.setdefault(key, {}), 33 | ) 34 | else: 35 | output[key] = value 36 | 37 | result = {} 38 | 39 | for key, value in flat.items(): 40 | _split(key, value, separator, result) 41 | 42 | return result 43 | 44 | 45 | class Environment(EnvironmentABC): 46 | def __init__(self, other=None): 47 | self._variables = {} 48 | 49 | if other is not None: 50 | self.update(other) 51 | 52 | def clone(self): 53 | return self.__class__(self._variables) 54 | 55 | def setvar(self, key, value): 56 | self.update({key: value}) 57 | # self._variables[key] = value 58 | 59 | def getvar(self, key, default=None): 60 | try: 61 | return self._variables[key] 62 | except KeyError: 63 | prefix = f"{key}." 64 | 65 | keys = [k for k in self._variables if k.startswith(prefix)] 66 | 67 | if len(keys) != 0: 68 | return Environment( 69 | { 70 | k.removeprefix(prefix): v 71 | for k, v in self._variables.items() 72 | if k.startswith(prefix) 73 | } 74 | ) 75 | 76 | return default 77 | 78 | def getvar_nodefault(self, key): 79 | return self._variables[key] 80 | 81 | def update(self, other, namespace=None): 82 | if isinstance(other, EnvironmentABC): 83 | if not namespace: 84 | self._variables.update(other._variables) 85 | return 86 | 87 | self._variables.update(flatten_nested_dict({namespace: other._variables})) 88 | else: 89 | if not namespace: 90 | self._variables.update(flatten_nested_dict(other)) 91 | return 92 | 93 | self._variables.update(flatten_nested_dict({namespace: other})) 94 | 95 | def asdict(self): 96 | return nest_flattened_dict(self._variables) 97 | 98 | def asflatdict(self): 99 | return self._variables 100 | 101 | def __repr__(self): # pragma: no cover 102 | return f"{self.asdict()}" 103 | 104 | def __eq__(self, other): 105 | return self._variables == other._variables 106 | -------------------------------------------------------------------------------- /mau/errors.py: -------------------------------------------------------------------------------- 1 | class MauErrorException(ValueError): 2 | """ 3 | This is a processing error that can occour 4 | during lexing, parsing, or visiting. 5 | """ 6 | 7 | def __init__(self, error): 8 | super().__init__(error.message) 9 | 10 | self.error = error 11 | 12 | 13 | class MauError: 14 | """ 15 | This is a processing error that can occour 16 | during lexing, parsing, or visiting. 17 | """ 18 | 19 | source = "global" 20 | 21 | def __init__(self, message, details=None): 22 | super().__init__() 23 | 24 | self.message = message 25 | 26 | # These are specific values needed by 27 | # this error, e.g. the context for 28 | # errors related to tokens 29 | self.details = details or {} 30 | 31 | def print_details(self): # pragma: no cover 32 | print(f"Error: {self.message}") 33 | 34 | 35 | def print_error(error): # pragma: no cover 36 | # This is a function used to print an error occurred 37 | # during processing. 38 | 39 | len_bar = 80 40 | title = f" {error.source} error " 41 | 42 | # This takes into account the title and the spaces 43 | half_bar = "=" * ((len_bar - len(title)) // 2 + 1) 44 | full_bar = half_bar + "=" * len(title) + half_bar 45 | 46 | print(f"{half_bar}{title}{half_bar}") 47 | 48 | error.print_details() 49 | 50 | print(f"{full_bar}") 51 | -------------------------------------------------------------------------------- /mau/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def rematch(regexp, text): 5 | # Compile the regexp and get a match on the current line 6 | return re.compile(regexp).match(text) 7 | -------------------------------------------------------------------------------- /mau/lexers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/lexers/__init__.py -------------------------------------------------------------------------------- /mau/lexers/arguments_lexer.py: -------------------------------------------------------------------------------- 1 | from mau.helpers import rematch 2 | from mau.lexers.base_lexer import BaseLexer, TokenTypes 3 | 4 | 5 | class ArgumentsLexer(BaseLexer): 6 | def _process_functions(self): 7 | return [ 8 | self._process_whitespace, 9 | self._process_literal, 10 | self._process_text, 11 | ] 12 | 13 | def _process_whitespace(self): 14 | match = rematch(r" +", self._tail) 15 | 16 | if not match: 17 | return None 18 | 19 | return [self._create_token_and_skip(TokenTypes.WHITESPACE, match.group())] 20 | 21 | def _process_literal(self): 22 | if self._current_char not in r'\=,"': 23 | return None 24 | 25 | return [self._create_token_and_skip(TokenTypes.LITERAL, self._current_char)] 26 | 27 | def _process_text(self): 28 | match = rematch(r'[^\\=," ]+', self._tail) 29 | 30 | if not match: # pragma: no cover 31 | return None 32 | 33 | return [self._create_token_and_skip(TokenTypes.TEXT, match.group())] 34 | -------------------------------------------------------------------------------- /mau/lexers/preprocess_variables_lexer.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.base_lexer import BaseLexer, TokenTypes 2 | 3 | 4 | class PreprocessVariablesLexer(BaseLexer): 5 | def _process_literal(self): 6 | if self._current_char not in r"\`{}": 7 | return None 8 | 9 | return [self._create_token_and_skip(TokenTypes.LITERAL, self._current_char)] 10 | 11 | def _process_text(self): 12 | return [self._create_token_and_skip(TokenTypes.TEXT, self._current_char)] 13 | 14 | def _process_functions(self): 15 | return [ 16 | self._process_literal, 17 | self._process_text, 18 | ] 19 | -------------------------------------------------------------------------------- /mau/lexers/text_lexer.py: -------------------------------------------------------------------------------- 1 | from mau.helpers import rematch 2 | from mau.lexers.base_lexer import BaseLexer, TokenTypes 3 | 4 | 5 | class TextLexer(BaseLexer): 6 | def _process_whitespace(self): 7 | match = rematch(r"\ +", self._tail) 8 | 9 | if not match: 10 | return None 11 | 12 | return [self._create_token_and_skip(TokenTypes.TEXT, " ")] 13 | 14 | def _process_literal(self): 15 | if self._current_char not in '~^_*`{}()[]\\"$%': 16 | return None 17 | 18 | return [self._create_token_and_skip(TokenTypes.LITERAL, self._current_char)] 19 | 20 | def _process_text(self): 21 | match = rematch(r'[^~\^_*`{}()[\]"\\ \$%]+', self._tail) 22 | 23 | if not match: # pragma: no cover 24 | return None 25 | 26 | return [self._create_token_and_skip(TokenTypes.TEXT, match.group())] 27 | 28 | def _process_functions(self): 29 | return [ 30 | self._process_whitespace, 31 | self._process_literal, 32 | self._process_text, 33 | ] 34 | -------------------------------------------------------------------------------- /mau/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/nodes/__init__.py -------------------------------------------------------------------------------- /mau/nodes/arguments.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node, ValueNode 2 | 3 | 4 | class UnnamedArgumentNode(ValueNode): 5 | """ 6 | This node contains an unnamed argument. 7 | """ 8 | 9 | node_type = "unnamed_argument" 10 | 11 | 12 | class NamedArgumentNode(Node): 13 | """ 14 | This node contains a named argument. 15 | """ 16 | 17 | node_type = "named_argument" 18 | 19 | def __init__( 20 | self, 21 | key, 22 | value, 23 | parent=None, 24 | parent_position=None, 25 | children=None, 26 | subtype=None, 27 | args=None, 28 | kwargs=None, 29 | tags=None, 30 | context=None, 31 | ): 32 | super().__init__( 33 | parent=parent, 34 | parent_position=parent_position, 35 | children=children, 36 | subtype=subtype, 37 | args=args, 38 | kwargs=kwargs, 39 | tags=tags, 40 | context=context, 41 | ) 42 | self.key = key 43 | self.value = value 44 | 45 | def _custom_dict(self): 46 | return { 47 | "key": self.key, 48 | "value": self.value, 49 | } 50 | -------------------------------------------------------------------------------- /mau/nodes/block.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class BlockNode(Node): 5 | """A block. 6 | 7 | This node contains a generic block. 8 | 9 | Arguments: 10 | blocktype: the type of this block 11 | content: content of the block 12 | secondary_content: secondary content of this block 13 | title: title of this block 14 | classes: a comma-separated list of classes 15 | engine: the engine used to render this block 16 | preprocessor: the preprocessor used for this block 17 | args: unnamed arguments 18 | kwargs: named arguments 19 | tags: tags 20 | """ 21 | 22 | node_type = "block" 23 | 24 | def __init__( 25 | self, 26 | classes=None, 27 | title=None, 28 | engine=None, 29 | preprocessor=None, 30 | parent=None, 31 | parent_position=None, 32 | children=None, 33 | secondary_children=None, 34 | subtype=None, 35 | args=None, 36 | kwargs=None, 37 | tags=None, 38 | context=None, 39 | ): 40 | super().__init__( 41 | parent=parent, 42 | parent_position=parent_position, 43 | children=children, 44 | subtype=subtype, 45 | args=args, 46 | kwargs=kwargs, 47 | tags=tags, 48 | context=context, 49 | ) 50 | self.classes = classes or [] 51 | self.title = title 52 | self.engine = engine 53 | self.preprocessor = preprocessor 54 | self.secondary_children = secondary_children or [] 55 | 56 | def _custom_dict(self): 57 | return { 58 | "classes": self.classes, 59 | "title": self.title, 60 | "engine": self.engine, 61 | "preprocessor": self.preprocessor, 62 | "secondary_children": self.secondary_children, 63 | } 64 | 65 | 66 | class BlockGroupNode(Node): 67 | """This instructs Mau to insert a group of nodes.""" 68 | 69 | node_type = "block_group" 70 | 71 | def __init__( 72 | self, 73 | group_name, 74 | group, 75 | title=None, 76 | parent=None, 77 | parent_position=None, 78 | children=None, 79 | subtype=None, 80 | args=None, 81 | kwargs=None, 82 | tags=None, 83 | context=None, 84 | ): 85 | super().__init__( 86 | parent=parent, 87 | parent_position=parent_position, 88 | children=children, 89 | subtype=subtype, 90 | args=args, 91 | kwargs=kwargs, 92 | tags=tags, 93 | context=context, 94 | ) 95 | self.title = title 96 | self.group_name = group_name 97 | self.group = group 98 | 99 | def _custom_dict(self): 100 | return { 101 | "title": self.title, 102 | "group_name": self.group_name, 103 | "group": self.group, 104 | } 105 | -------------------------------------------------------------------------------- /mau/nodes/content.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.page import Node 2 | 3 | 4 | class ContentNode(Node): 5 | """Content included in the page. 6 | 7 | This represents generic content included in the page. 8 | 9 | Arguments: 10 | content_type: the type of content 11 | title: caption of the image 12 | args: unnamed arguments 13 | kwargs: named arguments 14 | tags: tags 15 | """ 16 | 17 | node_type = "content" 18 | 19 | def __init__( 20 | self, 21 | content_type, 22 | uris, 23 | title=None, 24 | parent=None, 25 | parent_position=None, 26 | children=None, 27 | subtype=None, 28 | args=None, 29 | kwargs=None, 30 | tags=None, 31 | context=None, 32 | ): 33 | super().__init__( 34 | parent=parent, 35 | parent_position=parent_position, 36 | children=children, 37 | subtype=subtype, 38 | args=args, 39 | kwargs=kwargs, 40 | tags=tags, 41 | context=context, 42 | ) 43 | self.content_type = content_type 44 | self.title = title 45 | self.uris = uris 46 | 47 | def _custom_dict(self): 48 | return { 49 | "content_type": self.content_type, 50 | "title": self.title, 51 | "uris": self.uris, 52 | } 53 | 54 | 55 | class ContentImageNode(Node): 56 | """An image included in the page.""" 57 | 58 | node_type = "content_image" 59 | 60 | def __init__( 61 | self, 62 | uri, 63 | alt_text=None, 64 | classes=None, 65 | title=None, 66 | parent=None, 67 | parent_position=None, 68 | children=None, 69 | subtype=None, 70 | args=None, 71 | kwargs=None, 72 | tags=None, 73 | context=None, 74 | ): 75 | super().__init__( 76 | parent=parent, 77 | parent_position=parent_position, 78 | children=children, 79 | subtype=subtype, 80 | args=args, 81 | kwargs=kwargs, 82 | tags=tags, 83 | context=context, 84 | ) 85 | self.uri = uri 86 | self.title = title 87 | self.alt_text = alt_text 88 | self.classes = classes 89 | 90 | def _custom_dict(self): 91 | return { 92 | "uri": self.uri, 93 | "title": self.title, 94 | "alt_text": self.alt_text, 95 | "classes": self.classes, 96 | } 97 | -------------------------------------------------------------------------------- /mau/nodes/footnotes.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class FootnoteNode(Node): 5 | node_type = "footnote" 6 | 7 | def __init__( 8 | self, 9 | number=None, 10 | reference_anchor=None, 11 | content_anchor=None, 12 | parent=None, 13 | parent_position=None, 14 | children=None, 15 | subtype=None, 16 | args=None, 17 | kwargs=None, 18 | tags=None, 19 | context=None, 20 | ): 21 | super().__init__( 22 | parent=parent, 23 | parent_position=parent_position, 24 | children=children, 25 | subtype=subtype, 26 | args=args, 27 | kwargs=kwargs, 28 | tags=tags, 29 | context=context, 30 | ) 31 | self.number = number 32 | self.reference_anchor = reference_anchor 33 | self.content_anchor = content_anchor 34 | 35 | def _custom_dict(self): 36 | return { 37 | "number": self.number, 38 | "reference_anchor": self.reference_anchor, 39 | "content_anchor": self.content_anchor, 40 | } 41 | 42 | def to_entry(self): 43 | return FootnotesEntryNode( 44 | number=self.number, 45 | reference_anchor=self.reference_anchor, 46 | content_anchor=self.content_anchor, 47 | parent=self.parent, 48 | parent_position=self.parent_position, 49 | children=self.children, 50 | subtype=self.subtype, 51 | args=self.args, 52 | kwargs=self.kwargs, 53 | tags=self.tags, 54 | context=self.context, 55 | ) 56 | 57 | 58 | class MacroFootnoteNode(FootnoteNode): 59 | """A footnote created inside a piece of text.""" 60 | 61 | node_type = "footnote" 62 | 63 | 64 | class FootnotesEntryNode(FootnoteNode): 65 | """An entry of the list of footnotes.""" 66 | 67 | node_type = "footnotes_entry" 68 | 69 | 70 | class FootnotesNode(Node): 71 | """This instructs Mau to insert the list of footnotes.""" 72 | 73 | node_type = "footnotes" 74 | -------------------------------------------------------------------------------- /mau/nodes/header.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class HeaderNode(Node): 5 | """A header.""" 6 | 7 | node_type = "header" 8 | 9 | def __init__( 10 | self, 11 | value=None, 12 | level=None, 13 | anchor=None, 14 | parent=None, 15 | parent_position=None, 16 | children=None, 17 | subtype=None, 18 | args=None, 19 | kwargs=None, 20 | tags=None, 21 | context=None, 22 | ): 23 | super().__init__( 24 | parent=parent, 25 | parent_position=parent_position, 26 | children=children, 27 | subtype=subtype, 28 | args=args, 29 | kwargs=kwargs, 30 | tags=tags, 31 | context=context, 32 | ) 33 | self.value = value 34 | self.level = level 35 | self.anchor = anchor 36 | 37 | def _custom_dict(self): 38 | return { 39 | "value": self.value, 40 | "level": self.level, 41 | "anchor": self.anchor, 42 | } 43 | -------------------------------------------------------------------------------- /mau/nodes/inline.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node, ValueNode 2 | 3 | 4 | class WordNode(ValueNode): 5 | """This is a single word, it's used internally 6 | and eventually packed together with others into 7 | a TextNode 8 | """ 9 | 10 | node_type = "word" 11 | 12 | 13 | class TextNode(ValueNode): 14 | """This contains plain text and is created 15 | as a collation of multiple WordNode objects 16 | """ 17 | 18 | node_type = "text" 19 | 20 | 21 | class RawNode(ValueNode): 22 | """This contains plain text but the content 23 | should be treated as raw data and left untouched. 24 | E.g. it shouldn't be escaped. 25 | """ 26 | 27 | node_type = "raw" 28 | 29 | 30 | class VerbatimNode(ValueNode): 31 | """This node contains verbatim text.""" 32 | 33 | node_type = "verbatim" 34 | 35 | 36 | class SentenceNode(Node): 37 | """A recursive container node. 38 | 39 | This node represents the content of a paragraph, but it is recursive, 40 | while ParagraphNode is not. 41 | """ 42 | 43 | node_type = "sentence" 44 | 45 | 46 | class StyleNode(ValueNode): 47 | """Describes the style applied to a node.""" 48 | 49 | node_type = "style" 50 | -------------------------------------------------------------------------------- /mau/nodes/lists.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class ListItemNode(Node): 5 | """An entry in a list.""" 6 | 7 | node_type = "list_item" 8 | 9 | def __init__( 10 | self, 11 | level, 12 | parent=None, 13 | parent_position=None, 14 | children=None, 15 | subtype=None, 16 | args=None, 17 | kwargs=None, 18 | tags=None, 19 | context=None, 20 | ): 21 | super().__init__( 22 | parent=parent, 23 | parent_position=parent_position, 24 | children=children, 25 | subtype=subtype, 26 | args=args, 27 | kwargs=kwargs, 28 | tags=tags, 29 | context=context, 30 | ) 31 | self.level = level 32 | 33 | def _custom_dict(self): 34 | return { 35 | "level": self.level, 36 | } 37 | 38 | 39 | class ListNode(Node): 40 | """A list.""" 41 | 42 | node_type = "list" 43 | 44 | def __init__( 45 | self, 46 | ordered, 47 | main_node=False, 48 | start=1, 49 | parent=None, 50 | parent_position=None, 51 | children=None, 52 | subtype=None, 53 | args=None, 54 | kwargs=None, 55 | tags=None, 56 | context=None, 57 | ): 58 | super().__init__( 59 | parent=parent, 60 | parent_position=parent_position, 61 | children=children, 62 | subtype=subtype, 63 | args=args, 64 | kwargs=kwargs, 65 | tags=tags, 66 | context=context, 67 | ) 68 | self.ordered = ordered 69 | self.main_node = main_node 70 | self.start = start 71 | 72 | def _custom_dict(self): 73 | return { 74 | "ordered": self.ordered, 75 | "main_node": self.main_node, 76 | "start": self.start, 77 | } 78 | -------------------------------------------------------------------------------- /mau/nodes/nodes.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | class Node: 5 | node_type = "node" 6 | 7 | def __init__( 8 | self, 9 | parent=None, 10 | parent_position=None, 11 | children=None, 12 | subtype=None, 13 | args=None, 14 | kwargs=None, 15 | tags=None, 16 | context=None, 17 | ): 18 | self.parent = parent 19 | self.parent_position = parent_position 20 | self.subtype = subtype 21 | self.children = children or [] 22 | self.args = args or [] 23 | self.kwargs = kwargs or {} 24 | self.tags = tags or [] 25 | self.context = context 26 | 27 | def _custom_dict(self): 28 | return {} 29 | 30 | def add_children(self, children): 31 | self.children.extend(children) 32 | for child in children: 33 | child.parent = self 34 | 35 | def asdict(self): 36 | base = { 37 | # Parent is excluded to avoid 38 | # having to deal with recursion 39 | "type": self.node_type, 40 | "subtype": self.subtype, 41 | "children": [i.asdict() for i in self.children], 42 | "args": self.args, 43 | "kwargs": self.kwargs, 44 | "tags": self.tags, 45 | } 46 | base.update(self._custom_dict()) 47 | 48 | return base 49 | 50 | def accept(self, visitor, *args, **kwargs): 51 | # Some node types contain a dot to allow templates 52 | # to be created in a hierarchy of directories 53 | # but dots are not allowed in function names 54 | method_name = f"_visit_{self.node_type.replace('.', '__')}" 55 | 56 | try: 57 | method = getattr(visitor, method_name) 58 | except AttributeError: 59 | method = getattr(visitor, "_visit_default") 60 | 61 | return method(self, *args, **kwargs) 62 | 63 | def __eq__(self, other): 64 | try: 65 | return self.asdict() == other.asdict() 66 | except AttributeError: # pragma: no cover 67 | return False 68 | 69 | def __repr__(self): # pragma: no cover 70 | return str(self.asdict()) 71 | 72 | def hash(self): # pragma: no cover 73 | return hashlib.md5(str(self).encode("utf-8")).hexdigest()[:8] 74 | 75 | 76 | class ValueNode(Node): 77 | node_type = "value_node" 78 | 79 | def __init__( 80 | self, 81 | value, 82 | parent=None, 83 | parent_position=None, 84 | children=None, 85 | subtype=None, 86 | args=None, 87 | kwargs=None, 88 | tags=None, 89 | context=None, 90 | ): 91 | super().__init__( 92 | parent=parent, 93 | parent_position=parent_position, 94 | children=children, 95 | subtype=subtype, 96 | args=args, 97 | kwargs=kwargs, 98 | tags=tags, 99 | context=context, 100 | ) 101 | self.value = value 102 | 103 | def _custom_dict(self): 104 | return { 105 | "value": self.value, 106 | } 107 | -------------------------------------------------------------------------------- /mau/nodes/page.py: -------------------------------------------------------------------------------- 1 | # Page nodes can be found at top level in a page 2 | 3 | from mau.nodes.nodes import Node 4 | 5 | 6 | class HorizontalRuleNode(Node): 7 | """A horizontal rule.""" 8 | 9 | node_type = "horizontal_rule" 10 | 11 | 12 | class ContainerNode(Node): 13 | node_type = "container" 14 | 15 | def clone(self): 16 | return self.__class__( 17 | parent=self.parent, 18 | parent_position=self.parent_position, 19 | children=self.children, 20 | subtype=self.subtype, 21 | args=self.args, 22 | kwargs=self.kwargs, 23 | tags=self.tags, 24 | context=self.context, 25 | ) 26 | 27 | 28 | class DocumentNode(ContainerNode): 29 | """A document. 30 | 31 | This node represents the full document. 32 | 33 | Arguments: 34 | content: the content of the document 35 | """ 36 | 37 | node_type = "document" 38 | -------------------------------------------------------------------------------- /mau/nodes/paragraph.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class ParagraphNode(Node): 5 | """A paragraph.""" 6 | 7 | node_type = "paragraph" 8 | 9 | def __init__( 10 | self, 11 | title=None, 12 | parent=None, 13 | parent_position=None, 14 | children=None, 15 | subtype=None, 16 | args=None, 17 | kwargs=None, 18 | tags=None, 19 | context=None, 20 | ): 21 | super().__init__( 22 | parent=parent, 23 | parent_position=parent_position, 24 | children=children, 25 | subtype=subtype, 26 | args=args, 27 | kwargs=kwargs, 28 | tags=tags, 29 | context=context, 30 | ) 31 | self.title = title 32 | 33 | def _custom_dict(self): 34 | return { 35 | "title": self.title, 36 | } 37 | -------------------------------------------------------------------------------- /mau/nodes/toc.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.nodes import Node 2 | 3 | 4 | class TocEntryNode(Node): 5 | """An entry of the Table of Contents. 6 | 7 | This node contains an entry of the Table of Contents. 8 | """ 9 | 10 | node_type = "toc_entry" 11 | 12 | def __init__( 13 | self, 14 | value=None, 15 | anchor=None, 16 | parent=None, 17 | parent_position=None, 18 | children=None, 19 | subtype=None, 20 | args=None, 21 | kwargs=None, 22 | tags=None, 23 | context=None, 24 | ): 25 | super().__init__( 26 | parent=parent, 27 | parent_position=parent_position, 28 | children=children, 29 | subtype=subtype, 30 | args=args, 31 | kwargs=kwargs, 32 | tags=tags, 33 | context=context, 34 | ) 35 | self.value = value 36 | self.anchor = anchor 37 | 38 | def _custom_dict(self): 39 | return { 40 | "value": self.value, 41 | "anchor": self.anchor, 42 | } 43 | 44 | 45 | class TocNode(Node): 46 | """A Table of Contents command. 47 | 48 | This node contains the headers that go into the ToC. 49 | """ 50 | 51 | node_type = "toc" 52 | -------------------------------------------------------------------------------- /mau/parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/parsers/__init__.py -------------------------------------------------------------------------------- /mau/parsers/arguments.py: -------------------------------------------------------------------------------- 1 | def set_names_and_defaults( 2 | args, 3 | kwargs, 4 | positional_names, 5 | default_values=None, 6 | ): 7 | """ 8 | Gives names to positional arguments and assigns 9 | default values to the ones that have not been 10 | initialised. 11 | """ 12 | 13 | if default_values is not None: 14 | _default_values = default_values.copy() 15 | else: 16 | _default_values = {} 17 | 18 | # If a named argument provides the value for a 19 | # positional name we consider it set 20 | positional_names = [i for i in positional_names if i not in kwargs] 21 | 22 | # If we pass more positional values than names, 23 | # some of them won't be converted and become flags 24 | remaining_args = args[len(positional_names) :] 25 | 26 | positional_arguments = dict(zip(positional_names, args)) 27 | 28 | # Named arguments win over the defaults 29 | _default_values.update(kwargs) 30 | 31 | # Positional arguments with win over all the rest 32 | _default_values.update(positional_arguments) 33 | 34 | # Positional arguments are mandatory and strict 35 | # so all the names have to be present in the 36 | # final dictionary. 37 | if not set(positional_names).issubset(set(_default_values.keys())): 38 | raise ValueError( 39 | f"The following attributes need to be specified: {positional_names}" 40 | ) 41 | 42 | return remaining_args, _default_values 43 | -------------------------------------------------------------------------------- /mau/parsers/attributes.py: -------------------------------------------------------------------------------- 1 | class AttributesManager: 2 | def __init__(self, parser): 3 | self.args = [] 4 | self.kwargs = {} 5 | self.tags = [] 6 | self.subtype = None 7 | 8 | self.parser = parser 9 | 10 | @property 11 | def attributes(self): 12 | return ( 13 | self.args, 14 | self.kwargs, 15 | self.tags, 16 | self.subtype, 17 | ) 18 | 19 | def push( 20 | self, 21 | args=None, 22 | kwargs=None, 23 | tags=None, 24 | subtype=None, 25 | ): 26 | self.args = args or self.args 27 | self.kwargs = kwargs or self.kwargs 28 | self.tags = tags or self.tags 29 | self.subtype = subtype or self.subtype 30 | 31 | def pop(self): 32 | args, kwargs, tags, subtype = self.attributes 33 | 34 | self.args = [] 35 | self.kwargs = {} 36 | self.tags = [] 37 | self.subtype = None 38 | 39 | return (args, kwargs, tags, subtype) 40 | -------------------------------------------------------------------------------- /mau/parsers/internal_links.py: -------------------------------------------------------------------------------- 1 | class InternalLinksManager: 2 | def __init__(self, parser): 3 | # This list containes the internal links created 4 | # in the text through a macro. 5 | self.links = [] 6 | 7 | # This dictionary contains the headers 8 | # flagged with an id 9 | self.headers = {} 10 | 11 | # This is the parser that contains the manager 12 | self.parser = parser 13 | 14 | def add_header(self, header_id, node): 15 | if header_id in self.headers: 16 | self.parser._error(f"Duplicate header id detected: {header_id}") 17 | 18 | self.headers[header_id] = node 19 | 20 | def process_links(self): 21 | for link in self.links: 22 | try: 23 | link.header = self.headers[link.header_id] 24 | except KeyError: 25 | self.parser._error(f"Cannot find header with id {link.header_id}") 26 | 27 | def update(self, other): 28 | self.update_links(other.links) 29 | self.headers.update(other.headers) 30 | 31 | def update_links(self, links): 32 | self.links.extend(links) 33 | -------------------------------------------------------------------------------- /mau/parsers/preprocess_variables_parser.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.base_lexer import TokenTypes 2 | from mau.lexers.preprocess_variables_lexer import PreprocessVariablesLexer 3 | from mau.nodes.inline import TextNode 4 | from mau.parsers.base_parser import BaseParser 5 | from mau.tokens.tokens import Token 6 | 7 | 8 | class PreprocessVariablesParser(BaseParser): 9 | lexer_class = PreprocessVariablesLexer 10 | 11 | def __init__(self, environment): 12 | super().__init__(environment) 13 | 14 | def _process_verbatim(self): 15 | self._get_token(TokenTypes.LITERAL, "`") 16 | text = self._collect_join( 17 | [Token(TokenTypes.LITERAL, "`")], 18 | preserve_escaped_stop_tokens=True, 19 | ) 20 | self._get_token(TokenTypes.LITERAL, "`") 21 | 22 | text = f"`{text}`" 23 | 24 | self._save(TextNode(text)) 25 | 26 | return True 27 | 28 | def _process_escaped_char(self): 29 | self._get_token(TokenTypes.LITERAL, "\\") 30 | 31 | char = self._get_token().value 32 | 33 | if char not in "{}": 34 | char = f"\\{char}" 35 | 36 | self._save(TextNode(char)) 37 | 38 | return True 39 | 40 | def _process_curly(self): 41 | self._get_token(TokenTypes.LITERAL, "{") 42 | variable_name = self._collect_join(stop_tokens=[Token(TokenTypes.LITERAL, "}")]) 43 | self._get_token(TokenTypes.LITERAL, "}") 44 | 45 | try: 46 | variable_value = self.environment.getvar_nodefault(variable_name) 47 | 48 | # Boolean variables are used in 49 | # conditions but shouldn't be printed 50 | if variable_value in [True, False]: 51 | variable_value = "" 52 | 53 | self._save(TextNode(variable_value)) 54 | except KeyError: 55 | self._error(f'Attribute "{variable_name}" has not been defined') 56 | 57 | return True 58 | 59 | def _process_pass(self): 60 | self._save(TextNode(self._get_token().value)) 61 | 62 | return True 63 | 64 | def _process_functions(self): 65 | return [ 66 | self._process_escaped_char, 67 | self._process_verbatim, 68 | self._process_curly, 69 | self._process_pass, 70 | ] 71 | 72 | def parse(self, tokens): 73 | super().parse(tokens) 74 | 75 | # After having parsed the text and replaced the 76 | # variables, this should return a piece of text again 77 | text = "".join([str(i.value) for i in self.nodes]) 78 | self.nodes = [TextNode(text)] 79 | -------------------------------------------------------------------------------- /mau/test_helpers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pathlib 3 | 4 | 5 | import textwrap 6 | 7 | from mau.environment.environment import Environment 8 | from mau.text_buffer.context import Context 9 | from mau.text_buffer.text_buffer import TextBuffer 10 | 11 | 12 | def dedent(text): 13 | return textwrap.dedent(text).strip() 14 | 15 | 16 | def init_lexer_factory(lexer_class): 17 | """ 18 | A factory that returns a lexer initialiser. 19 | The returned function initialises the lexer 20 | and returns it. 21 | """ 22 | 23 | def _init_lexer(environment=None): 24 | return lexer_class(environment) 25 | 26 | return _init_lexer 27 | 28 | 29 | def lexer_runner_factory(lexer_class, *args, **kwds): 30 | """ 31 | A factory that returns a lexer runner. 32 | The returned function initialises and 33 | runs the lexer on the given source. 34 | """ 35 | 36 | init_lexer = init_lexer_factory(lexer_class) 37 | 38 | def _run(source, environment=None, **kwargs): 39 | kwds.update(kwargs) 40 | 41 | environment = environment or Environment() 42 | 43 | text_buffer = TextBuffer(textwrap.dedent(source), Context()) 44 | 45 | lexer = init_lexer(environment, *args, **kwds) 46 | lexer.process(text_buffer) 47 | 48 | return lexer 49 | 50 | return _run 51 | 52 | 53 | def init_parser_factory(lexer_class, parser_class): 54 | """ 55 | A factory that returns a parser initialiser. 56 | The returned function initialises and runs the lexer, 57 | initialises the parser and returns it. 58 | """ 59 | 60 | def _init_parser(text, environment=None, *args, **kwargs): 61 | text_buffer = TextBuffer(text, Context(source="main")) 62 | 63 | lex = lexer_class(environment) 64 | lex.process(text_buffer) 65 | 66 | par = parser_class(environment, *args, **kwargs) 67 | par.tokens = lex.tokens 68 | 69 | return par 70 | 71 | return _init_parser 72 | 73 | 74 | def parser_runner_factory(lexer_class, parser_class, *args, **kwds): 75 | """ 76 | A factory that returns a parser runner. 77 | The returned function runs the parser on the given source. 78 | """ 79 | 80 | init_parser = init_parser_factory(lexer_class, parser_class) 81 | 82 | def _run(source, environment=None, **kwargs): 83 | kwds.update(kwargs) 84 | 85 | environment = environment or Environment() 86 | 87 | parser = init_parser(textwrap.dedent(source), environment, *args, **kwds) 88 | parser.parse(parser.tokens) 89 | parser.finalise() 90 | 91 | return parser 92 | 93 | return _run 94 | 95 | 96 | def load_files(source_dir, source_ext, expected_dir, expected_ext): 97 | source_files = pathlib.Path(source_dir).rglob(f"*.{source_ext}") 98 | 99 | files = [ 100 | (i, expected_dir / i.with_suffix(f".{expected_ext}").relative_to(source_dir)) 101 | for i in source_files 102 | ] 103 | 104 | return files 105 | 106 | 107 | def collect_test_files( 108 | tests_dir, source_dir_name, source_ext, expected_dir_name, expected_ext 109 | ): 110 | source_dir = tests_dir / source_dir_name 111 | expected_dir = tests_dir / expected_dir_name 112 | 113 | return [ 114 | pytest.param(i, j, id=str(i.relative_to(tests_dir))) 115 | for i, j in load_files(source_dir, source_ext, expected_dir, expected_ext) 116 | ] 117 | -------------------------------------------------------------------------------- /mau/text_buffer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/text_buffer/__init__.py -------------------------------------------------------------------------------- /mau/text_buffer/context.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class Context: 7 | # Context objects represent the place where a token was found 8 | # in the source code. They contain line and column where they 9 | # begin, the name of the source file (if provided), and the 10 | # text of the full line. 11 | 12 | line: int = 0 13 | column: int = 0 14 | source: Optional[str] = None 15 | 16 | def asdict(self): 17 | return { 18 | "line": self.line, 19 | "column": self.column, 20 | "source": self.source, 21 | } 22 | 23 | def __repr__(self): 24 | return f"Context({self.line},{self.column},{self.source})" 25 | 26 | 27 | def print_context(context): # pragma: no cover 28 | # This is a function used to print out the context of a token 29 | # when an error occurs in Mau. It's a typical graphical 30 | # error-reporting function: a mess. 31 | 32 | line = context.line 33 | column = context.column 34 | 35 | if context.source is not None: 36 | print(f"Source: {context.source}") 37 | 38 | print(f"Line: {line} - Column: {column}") 39 | print() 40 | 41 | print(f"{line}: {context.text}") 42 | print(" " * (len(str(line)) + 2) + "^") 43 | -------------------------------------------------------------------------------- /mau/tokens/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/tokens/__init__.py -------------------------------------------------------------------------------- /mau/tokens/tokens.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from mau.text_buffer.context import Context 4 | 5 | 6 | class Token: 7 | """ 8 | This represents a token. 9 | Tokens have a type, a value (the actual characters), and a context 10 | """ 11 | 12 | def __init__( 13 | self, _type: str, value: Optional[str] = None, context: Optional[Context] = None 14 | ): 15 | self.type = _type 16 | self.context = context or Context() 17 | self.value = value or "" 18 | 19 | def __repr__(self): 20 | return f'Token({self.type}, "{self.value}", {self.context})' 21 | 22 | def __eq__(self, other): 23 | try: 24 | if self.value == "" or other.value == "": 25 | return self.type == other.type 26 | 27 | return (self.type, self.value) == ( 28 | other.type, 29 | other.value, 30 | ) 31 | except AttributeError: 32 | return False 33 | 34 | def __hash__(self): 35 | return hash((self.type, self.value)) 36 | 37 | def __len__(self): 38 | if self.value: 39 | return len(self.value) 40 | 41 | return 0 42 | 43 | def __bool__(self): 44 | return True 45 | 46 | def match(self, other): 47 | # Match is different from __eq__ because it also 48 | # checks the context. 49 | return self == other and self.context == other.context 50 | -------------------------------------------------------------------------------- /mau/visitors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/mau/visitors/__init__.py -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | 4 | @nox.session(python=["3.7", "3.8", "3.9", "3.10"]) 5 | def tests(session): 6 | session.install(".[testing]") 7 | session.run("pytest", "-svv") 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "mau" 7 | version = "4.0.2" 8 | description = "A lightweight template-driven markup language" 9 | authors = [{name = "Leonardo Giordani", email = "giordani.leonardo@gmail.com"}] 10 | license = {file = "LICENSE"} 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "License :: OSI Approved :: MIT License", 14 | "Intended Audience :: Developers", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | ] 24 | requires-python = ">=3.7" 25 | dependencies = [ 26 | "Jinja2", 27 | "pyyaml", 28 | "tabulate", 29 | ] 30 | readme = "README.md" 31 | 32 | [project.urls] 33 | Home = "https://github.com/Project-Mau/mau" 34 | 35 | [project.optional-dependencies] 36 | testing = [ 37 | "coverage", 38 | "nox", 39 | "pytest", 40 | "pytest-cov", 41 | "pytest-icdiff", 42 | ] 43 | development = [ 44 | "mau[testing]", 45 | "black", 46 | "flake8", 47 | "flake8-pyproject", 48 | "flit", 49 | "isort", 50 | "pip", 51 | "pylint", 52 | "wheel", 53 | ] 54 | 55 | [project.scripts] 56 | mau = "mau.main:main" 57 | 58 | [tool.black] 59 | target-version = ["py37", "py38", "py39", "py310"] 60 | include = '\.pyi?$' 61 | 62 | [tool.coverage.run] 63 | source = ["mau"] 64 | omit=["mau/main.py", "mau/__init__.py"] 65 | 66 | [tool.coverage.report] 67 | fail_under = 80 68 | include = ["mau/*"] 69 | exclude_lines = [ 70 | 'if TYPE_CHECKING:', 71 | 'pragma: no cover' 72 | ] 73 | exclude_also = [ 74 | "def __hash__", 75 | "def __repr__", 76 | "def __str__", 77 | ] 78 | 79 | [tool.pylint.main] 80 | disable = [ 81 | "import-error", 82 | "missing-class-docstring", 83 | "missing-function-docstring", 84 | "missing-module-docstring", 85 | "protected-access", 86 | "too-few-public-methods", 87 | "too-many-arguments", 88 | "too-many-instance-attributes", 89 | "too-many-return-statements", 90 | "use-implicit-booleaness-not-comparison" 91 | ] 92 | 93 | [tool.isort] 94 | profile = "black" 95 | src_paths = ["src", "tests"] 96 | 97 | [tool.pytest.ini_options] 98 | addopts = """\ 99 | --cov mau \ 100 | --cov tests \ 101 | --cov-report term-missing \ 102 | --no-cov-on-fail \ 103 | """ 104 | 105 | 106 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/production.txt 2 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | .[development] 2 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | .[testing] 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/__init__.py -------------------------------------------------------------------------------- /tests/e2e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/e2e/__init__.py -------------------------------------------------------------------------------- /tests/e2e/expected/block/block_admonition.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | classes: [] 7 | content: 8 | - data: 9 | args: [] 10 | content: 11 | - data: 12 | args: [] 13 | kwargs: {} 14 | subtype: null 15 | tags: [] 16 | type: text 17 | value: 'This is an admonition, which is a specific type of block with 18 | a ' 19 | - data: 20 | args: [] 21 | kwargs: {} 22 | subtype: null 23 | tags: [] 24 | type: verbatim 25 | value: class 26 | - data: 27 | args: [] 28 | kwargs: {} 29 | subtype: null 30 | tags: [] 31 | type: text 32 | value: ', and ' 33 | - data: 34 | args: [] 35 | kwargs: {} 36 | subtype: null 37 | tags: [] 38 | type: verbatim 39 | value: icon 40 | - data: 41 | args: [] 42 | kwargs: {} 43 | subtype: null 44 | tags: [] 45 | type: text 46 | value: ', and a ' 47 | - data: 48 | args: [] 49 | kwargs: {} 50 | subtype: null 51 | tags: [] 52 | type: verbatim 53 | value: label 54 | - data: 55 | args: [] 56 | kwargs: {} 57 | subtype: null 58 | tags: [] 59 | type: text 60 | value: ' that work as a title. This block is implemented purely through 61 | Jinja templates.' 62 | kwargs: {} 63 | subtype: null 64 | tags: [] 65 | title: {} 66 | type: paragraph 67 | engine: null 68 | kwargs: 69 | class: warning 70 | icon: someicon 71 | preprocessor: none 72 | secondary_content: [] 73 | subtype: admonition 74 | tags: [] 75 | title: 76 | data: 77 | args: [] 78 | content: 79 | - data: 80 | args: [] 81 | kwargs: {} 82 | subtype: null 83 | tags: [] 84 | type: text 85 | value: somelabel 86 | kwargs: {} 87 | subtype: null 88 | tags: [] 89 | type: sentence 90 | type: block 91 | kwargs: {} 92 | subtype: null 93 | tags: [] 94 | type: document 95 | 96 | -------------------------------------------------------------------------------- /tests/e2e/expected/block/block_conditionals.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | classes: [] 7 | content: 8 | - data: 9 | args: [] 10 | content: 11 | - data: 12 | args: [] 13 | kwargs: {} 14 | subtype: null 15 | tags: [] 16 | type: text 17 | value: This will be rendered 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | title: {} 22 | type: paragraph 23 | engine: null 24 | kwargs: {} 25 | preprocessor: none 26 | secondary_content: [] 27 | subtype: aside 28 | tags: [] 29 | title: {} 30 | type: block 31 | kwargs: {} 32 | subtype: null 33 | tags: [] 34 | type: document 35 | 36 | -------------------------------------------------------------------------------- /tests/e2e/expected/block/block_quote.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | classes: [] 7 | content: 8 | - data: 9 | args: [] 10 | content: 11 | - data: 12 | args: [] 13 | kwargs: {} 14 | subtype: null 15 | tags: [] 16 | type: text 17 | value: "Some of the newer ones are having trouble because they never\ 18 | \ really mastered some basic techniques, but they\u2019re working\ 19 | \ hard and improving." 20 | kwargs: {} 21 | subtype: null 22 | tags: [] 23 | title: {} 24 | type: paragraph 25 | engine: null 26 | kwargs: {} 27 | preprocessor: none 28 | secondary_content: 29 | - data: 30 | args: [] 31 | content: 32 | - data: 33 | args: [] 34 | kwargs: {} 35 | subtype: null 36 | tags: [] 37 | type: text 38 | value: 'Orson Scott Card, ' 39 | - data: 40 | args: [] 41 | content: 42 | - data: 43 | args: [] 44 | kwargs: {} 45 | subtype: null 46 | tags: [] 47 | type: text 48 | value: Ender's Game 49 | kwargs: {} 50 | subtype: null 51 | tags: [] 52 | type: style 53 | value: underscore 54 | - data: 55 | args: [] 56 | kwargs: {} 57 | subtype: null 58 | tags: [] 59 | type: text 60 | value: ' (1985)' 61 | kwargs: {} 62 | subtype: null 63 | tags: [] 64 | title: {} 65 | type: paragraph 66 | subtype: quote 67 | tags: [] 68 | title: {} 69 | type: block 70 | kwargs: {} 71 | subtype: null 72 | tags: [] 73 | type: document 74 | 75 | -------------------------------------------------------------------------------- /tests/e2e/expected/block/block_simple.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | classes: [] 7 | content: 8 | - data: 9 | args: [] 10 | content: 11 | - data: 12 | args: [] 13 | kwargs: {} 14 | subtype: null 15 | tags: [] 16 | type: text 17 | value: This is a block without attributes. It should be rendered as 18 | a div, however. 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | title: {} 23 | type: paragraph 24 | engine: null 25 | kwargs: {} 26 | preprocessor: none 27 | secondary_content: [] 28 | subtype: null 29 | tags: [] 30 | title: {} 31 | type: block 32 | - data: 33 | args: [] 34 | classes: [] 35 | content: 36 | - data: 37 | args: [] 38 | content: 39 | - data: 40 | args: [] 41 | kwargs: {} 42 | subtype: null 43 | tags: [] 44 | type: text 45 | value: 'This is another block between two markers ' 46 | - data: 47 | args: [] 48 | kwargs: {} 49 | subtype: null 50 | tags: [] 51 | type: verbatim 52 | value: ++++ 53 | - data: 54 | args: [] 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: text 59 | value: . 60 | kwargs: {} 61 | subtype: null 62 | tags: [] 63 | title: {} 64 | type: paragraph 65 | engine: null 66 | kwargs: {} 67 | preprocessor: none 68 | secondary_content: [] 69 | subtype: null 70 | tags: [] 71 | title: {} 72 | type: block 73 | - data: 74 | args: [] 75 | content: 76 | - data: 77 | args: [] 78 | kwargs: {} 79 | subtype: null 80 | tags: [] 81 | type: text 82 | value: The four characters can be escaped 83 | kwargs: {} 84 | subtype: null 85 | tags: [] 86 | title: {} 87 | type: paragraph 88 | - data: 89 | args: [] 90 | content: 91 | - data: 92 | args: [] 93 | kwargs: {} 94 | subtype: null 95 | tags: [] 96 | type: text 97 | value: '----' 98 | kwargs: {} 99 | subtype: null 100 | tags: [] 101 | title: {} 102 | type: paragraph 103 | - data: 104 | args: [] 105 | classes: [] 106 | content: 107 | - data: 108 | args: [] 109 | content: 110 | - data: 111 | args: [] 112 | kwargs: {} 113 | subtype: null 114 | tags: [] 115 | type: text 116 | value: A block with a subtype 117 | kwargs: {} 118 | subtype: null 119 | tags: [] 120 | title: {} 121 | type: paragraph 122 | engine: null 123 | kwargs: {} 124 | preprocessor: none 125 | secondary_content: [] 126 | subtype: sometype 127 | tags: [] 128 | title: {} 129 | type: block 130 | kwargs: {} 131 | subtype: null 132 | tags: [] 133 | type: document 134 | 135 | -------------------------------------------------------------------------------- /tests/e2e/expected/block/block_title.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | classes: [] 7 | content: 8 | - data: 9 | args: [] 10 | content: 11 | - data: 12 | args: [] 13 | kwargs: {} 14 | subtype: null 15 | tags: [] 16 | type: text 17 | value: This is a block 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | title: {} 22 | type: paragraph 23 | engine: null 24 | kwargs: {} 25 | preprocessor: none 26 | secondary_content: [] 27 | subtype: null 28 | tags: [] 29 | title: 30 | data: 31 | args: [] 32 | content: 33 | - data: 34 | args: [] 35 | kwargs: {} 36 | subtype: null 37 | tags: [] 38 | type: text 39 | value: The title 40 | kwargs: {} 41 | subtype: null 42 | tags: [] 43 | type: sentence 44 | type: block 45 | kwargs: {} 46 | subtype: null 47 | tags: [] 48 | type: document 49 | 50 | -------------------------------------------------------------------------------- /tests/e2e/expected/commands/footnotes.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This file contains a footnote 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | content: 20 | - data: 21 | args: [] 22 | kwargs: {} 23 | subtype: null 24 | tags: [] 25 | type: text 26 | value: This is a footnote 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | title: {} 31 | type: paragraph 32 | content_anchor: cnt-footnote-1-02376935 33 | kwargs: {} 34 | number: 1 35 | reference_anchor: ref-footnote-1-02376935 36 | subtype: null 37 | tags: [] 38 | type: footnote 39 | kwargs: {} 40 | subtype: null 41 | tags: [] 42 | title: {} 43 | type: paragraph 44 | - data: 45 | args: [] 46 | entries: 47 | - data: 48 | args: [] 49 | content: 50 | - data: 51 | args: [] 52 | content: 53 | - data: 54 | args: [] 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: text 59 | value: This is a footnote 60 | kwargs: {} 61 | subtype: null 62 | tags: [] 63 | title: {} 64 | type: paragraph 65 | content_anchor: cnt-footnote-1-02376935 66 | kwargs: {} 67 | number: 1 68 | reference_anchor: ref-footnote-1-02376935 69 | subtype: null 70 | tags: [] 71 | type: footnotes_entry 72 | kwargs: {} 73 | subtype: null 74 | tags: [] 75 | type: footnotes 76 | kwargs: {} 77 | subtype: null 78 | tags: [] 79 | type: document 80 | 81 | -------------------------------------------------------------------------------- /tests/e2e/expected/commands/toc.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | anchor: test-toc-4aa7 6 | args: [] 7 | kwargs: {} 8 | level: 1 9 | subtype: null 10 | tags: [] 11 | type: header 12 | value: 13 | data: 14 | args: [] 15 | content: 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: text 22 | value: Test ToC 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: sentence 27 | - data: 28 | args: [] 29 | entries: 30 | - data: 31 | anchor: test-toc-4aa7 32 | children: [] 33 | tags: [] 34 | type: toc_entry 35 | value: 36 | data: 37 | args: [] 38 | content: 39 | - data: 40 | args: [] 41 | kwargs: {} 42 | subtype: null 43 | tags: [] 44 | type: text 45 | value: Test ToC 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: sentence 50 | kwargs: {} 51 | tags: [] 52 | type: toc 53 | kwargs: {} 54 | subtype: null 55 | tags: [] 56 | type: document 57 | 58 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/classes.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'This paragraph uses ' 14 | - data: 15 | args: [] 16 | classes: 17 | - class1 18 | - class2 19 | content: 20 | - data: 21 | args: [] 22 | kwargs: {} 23 | subtype: null 24 | tags: [] 25 | type: text 26 | value: classes 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | type: macro.class 31 | - data: 32 | args: [] 33 | kwargs: {} 34 | subtype: null 35 | tags: [] 36 | type: text 37 | value: . 38 | kwargs: {} 39 | subtype: null 40 | tags: [] 41 | title: {} 42 | type: paragraph 43 | kwargs: {} 44 | subtype: null 45 | tags: [] 46 | type: document 47 | 48 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/conditionals.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'A true statement is ' 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: text 23 | value: 'TRUE' 24 | kwargs: {} 25 | subtype: null 26 | tags: [] 27 | type: sentence 28 | - data: 29 | args: [] 30 | kwargs: {} 31 | subtype: null 32 | tags: [] 33 | type: text 34 | value: . 35 | kwargs: {} 36 | subtype: null 37 | tags: [] 38 | title: {} 39 | type: paragraph 40 | - data: 41 | args: [] 42 | content: 43 | - data: 44 | args: [] 45 | kwargs: {} 46 | subtype: null 47 | tags: [] 48 | type: text 49 | value: 'A false statement is ' 50 | - data: 51 | args: [] 52 | content: 53 | - data: 54 | args: [] 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: text 59 | value: 'FALSE' 60 | kwargs: {} 61 | subtype: null 62 | tags: [] 63 | type: sentence 64 | - data: 65 | args: [] 66 | kwargs: {} 67 | subtype: null 68 | tags: [] 69 | type: text 70 | value: . 71 | kwargs: {} 72 | subtype: null 73 | tags: [] 74 | title: {} 75 | type: paragraph 76 | kwargs: {} 77 | subtype: null 78 | tags: [] 79 | type: document 80 | 81 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/inline_image.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'A paragraph with an inline image ' 14 | - data: 15 | alt_text: null 16 | args: [] 17 | height: null 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: macro.image 22 | uri: https://www.thedigitalcatonline.com/images/global/favicon.jpg 23 | width: null 24 | - data: 25 | args: [] 26 | kwargs: {} 27 | subtype: null 28 | tags: [] 29 | type: text 30 | value: . 31 | kwargs: {} 32 | subtype: null 33 | tags: [] 34 | title: {} 35 | type: paragraph 36 | kwargs: {} 37 | subtype: null 38 | tags: [] 39 | type: document 40 | 41 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/internal_link.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'This is an ' 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: text 23 | value: internal link 24 | header: 25 | anchor: header-d1ef 26 | level: '1' 27 | value: 28 | data: 29 | args: [] 30 | content: 31 | - data: 32 | args: [] 33 | kwargs: {} 34 | subtype: null 35 | tags: [] 36 | type: text 37 | value: Header 38 | kwargs: {} 39 | subtype: null 40 | tags: [] 41 | type: sentence 42 | kwargs: {} 43 | subtype: null 44 | tags: [] 45 | type: macro.header 46 | - data: 47 | args: [] 48 | kwargs: {} 49 | subtype: null 50 | tags: [] 51 | type: text 52 | value: . 53 | kwargs: {} 54 | subtype: null 55 | tags: [] 56 | title: {} 57 | type: paragraph 58 | - data: 59 | anchor: header-d1ef 60 | args: [] 61 | kwargs: 62 | id: someid 63 | level: 1 64 | subtype: null 65 | tags: [] 66 | type: header 67 | value: 68 | data: 69 | args: [] 70 | content: 71 | - data: 72 | args: [] 73 | kwargs: {} 74 | subtype: null 75 | tags: [] 76 | type: text 77 | value: Header 78 | kwargs: {} 79 | subtype: null 80 | tags: [] 81 | type: sentence 82 | kwargs: {} 83 | subtype: null 84 | tags: [] 85 | type: document 86 | 87 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/link.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'Links like there was no ' 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: text 23 | value: tomorrow 24 | kwargs: {} 25 | subtype: null 26 | tags: [] 27 | target: https://www.thedigitalcatonline.com 28 | type: macro.link 29 | - data: 30 | args: [] 31 | kwargs: {} 32 | subtype: null 33 | tags: [] 34 | type: text 35 | value: . 36 | kwargs: {} 37 | subtype: null 38 | tags: [] 39 | title: {} 40 | type: paragraph 41 | - data: 42 | args: [] 43 | content: 44 | - data: 45 | args: [] 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: text 50 | value: 'Also without text: ' 51 | - data: 52 | args: [] 53 | content: 54 | - data: 55 | args: [] 56 | kwargs: {} 57 | subtype: null 58 | tags: [] 59 | type: text 60 | value: https://www.thedigitalcatonline.com 61 | kwargs: {} 62 | subtype: null 63 | tags: [] 64 | target: https://www.thedigitalcatonline.com 65 | type: macro.link 66 | - data: 67 | args: [] 68 | kwargs: {} 69 | subtype: null 70 | tags: [] 71 | type: text 72 | value: . 73 | kwargs: {} 74 | subtype: null 75 | tags: [] 76 | title: {} 77 | type: paragraph 78 | - data: 79 | args: [] 80 | content: 81 | - data: 82 | args: [] 83 | kwargs: {} 84 | subtype: null 85 | tags: [] 86 | type: text 87 | value: 'Link text should be parsed ' 88 | - data: 89 | args: [] 90 | content: 91 | - data: 92 | args: [] 93 | content: 94 | - data: 95 | args: [] 96 | kwargs: {} 97 | subtype: null 98 | tags: [] 99 | type: text 100 | value: tomorrow 101 | kwargs: {} 102 | subtype: null 103 | tags: [] 104 | type: style 105 | value: star 106 | kwargs: {} 107 | subtype: null 108 | tags: [] 109 | target: https://example.com 110 | type: macro.link 111 | - data: 112 | args: [] 113 | kwargs: {} 114 | subtype: null 115 | tags: [] 116 | type: text 117 | value: . 118 | kwargs: {} 119 | subtype: null 120 | tags: [] 121 | title: {} 122 | type: paragraph 123 | kwargs: {} 124 | subtype: null 125 | tags: [] 126 | type: document 127 | 128 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/mailto.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'You can use the macro ' 14 | - data: 15 | args: [] 16 | kwargs: {} 17 | subtype: null 18 | tags: [] 19 | type: verbatim 20 | value: mailto 21 | - data: 22 | args: [] 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: text 27 | value: ' to format email links. If you need to know more write me at ' 28 | - data: 29 | args: [] 30 | content: 31 | - data: 32 | args: [] 33 | kwargs: {} 34 | subtype: null 35 | tags: [] 36 | type: text 37 | value: info@example.com 38 | kwargs: {} 39 | subtype: null 40 | tags: [] 41 | target: mailto:info@example.com 42 | type: macro.link 43 | - data: 44 | args: [] 45 | kwargs: {} 46 | subtype: null 47 | tags: [] 48 | type: text 49 | value: . 50 | kwargs: {} 51 | subtype: null 52 | tags: [] 53 | title: {} 54 | type: paragraph 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: document 59 | 60 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/multiple_paragraphs.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This document contains multiple paragraphs. 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | - data: 20 | args: [] 21 | content: 22 | - data: 23 | args: [] 24 | kwargs: {} 25 | subtype: null 26 | tags: [] 27 | type: text 28 | value: They are separated by an empty line. 29 | kwargs: {} 30 | subtype: null 31 | tags: [] 32 | title: {} 33 | type: paragraph 34 | kwargs: {} 35 | subtype: null 36 | tags: [] 37 | type: document 38 | 39 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/single_paragraph.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This document contains a single paragraph. 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: document 23 | 24 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/split_paragraph.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This is a paragraph split in two lines. The missing empty line makes 14 | the two line belong to the same paragraph. 15 | kwargs: {} 16 | subtype: null 17 | tags: [] 18 | title: {} 19 | type: paragraph 20 | kwargs: {} 21 | subtype: null 22 | tags: [] 23 | type: document 24 | 25 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/styles.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'This paragraph contains ' 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: text 23 | value: underscore 24 | kwargs: {} 25 | subtype: null 26 | tags: [] 27 | type: style 28 | value: underscore 29 | - data: 30 | args: [] 31 | kwargs: {} 32 | subtype: null 33 | tags: [] 34 | type: text 35 | value: ' text.' 36 | kwargs: {} 37 | subtype: null 38 | tags: [] 39 | title: {} 40 | type: paragraph 41 | - data: 42 | args: [] 43 | content: 44 | - data: 45 | args: [] 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: text 50 | value: 'This paragraph contains ' 51 | - data: 52 | args: [] 53 | content: 54 | - data: 55 | args: [] 56 | kwargs: {} 57 | subtype: null 58 | tags: [] 59 | type: text 60 | value: star 61 | kwargs: {} 62 | subtype: null 63 | tags: [] 64 | type: style 65 | value: star 66 | - data: 67 | args: [] 68 | kwargs: {} 69 | subtype: null 70 | tags: [] 71 | type: text 72 | value: ' text.' 73 | kwargs: {} 74 | subtype: null 75 | tags: [] 76 | title: {} 77 | type: paragraph 78 | - data: 79 | args: [] 80 | content: 81 | - data: 82 | args: [] 83 | kwargs: {} 84 | subtype: null 85 | tags: [] 86 | type: text 87 | value: 'This paragraph contains ' 88 | - data: 89 | args: [] 90 | content: 91 | - data: 92 | args: [] 93 | kwargs: {} 94 | subtype: null 95 | tags: [] 96 | type: text 97 | value: tilde 98 | kwargs: {} 99 | subtype: null 100 | tags: [] 101 | type: style 102 | value: tilde 103 | - data: 104 | args: [] 105 | kwargs: {} 106 | subtype: null 107 | tags: [] 108 | type: text 109 | value: ' text.' 110 | kwargs: {} 111 | subtype: null 112 | tags: [] 113 | title: {} 114 | type: paragraph 115 | - data: 116 | args: [] 117 | content: 118 | - data: 119 | args: [] 120 | kwargs: {} 121 | subtype: null 122 | tags: [] 123 | type: text 124 | value: 'This paragraph contains ' 125 | - data: 126 | args: [] 127 | content: 128 | - data: 129 | args: [] 130 | kwargs: {} 131 | subtype: null 132 | tags: [] 133 | type: text 134 | value: caret 135 | kwargs: {} 136 | subtype: null 137 | tags: [] 138 | type: style 139 | value: caret 140 | - data: 141 | args: [] 142 | kwargs: {} 143 | subtype: null 144 | tags: [] 145 | type: text 146 | value: ' text.' 147 | kwargs: {} 148 | subtype: null 149 | tags: [] 150 | title: {} 151 | type: paragraph 152 | kwargs: {} 153 | subtype: null 154 | tags: [] 155 | type: document 156 | 157 | -------------------------------------------------------------------------------- /tests/e2e/expected/inline/verbatim.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'This paragraph contains ' 14 | - data: 15 | args: [] 16 | kwargs: {} 17 | subtype: null 18 | tags: [] 19 | type: verbatim 20 | value: verbatim 21 | - data: 22 | args: [] 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: text 27 | value: ' text.' 28 | kwargs: {} 29 | subtype: null 30 | tags: [] 31 | title: {} 32 | type: paragraph 33 | kwargs: {} 34 | subtype: null 35 | tags: [] 36 | type: document 37 | 38 | -------------------------------------------------------------------------------- /tests/e2e/expected/other/directives.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | anchor: test-directives-93d4 6 | args: [] 7 | kwargs: {} 8 | level: 1 9 | subtype: null 10 | tags: [] 11 | type: header 12 | value: 13 | data: 14 | args: [] 15 | content: 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: text 22 | value: Test directives 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: sentence 27 | - data: 28 | args: [] 29 | content: 30 | - data: 31 | args: [] 32 | kwargs: {} 33 | subtype: null 34 | tags: [] 35 | type: text 36 | value: This file tests Mau directives 37 | kwargs: {} 38 | subtype: null 39 | tags: [] 40 | title: {} 41 | type: paragraph 42 | - data: 43 | args: [] 44 | content: 45 | - data: 46 | args: [] 47 | kwargs: {} 48 | subtype: null 49 | tags: [] 50 | type: text 51 | value: This text has been included through a directive 52 | kwargs: {} 53 | subtype: null 54 | tags: [] 55 | title: {} 56 | type: paragraph 57 | kwargs: {} 58 | subtype: null 59 | tags: [] 60 | type: document 61 | 62 | -------------------------------------------------------------------------------- /tests/e2e/expected/other/variable_advanced.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: 'A variable can contain Mau code: ' 14 | - data: 15 | args: [] 16 | content: 17 | - data: 18 | args: [] 19 | kwargs: {} 20 | subtype: null 21 | tags: [] 22 | type: text 23 | value: https://en.wikipedia.org/wiki/42_(number) 24 | kwargs: {} 25 | subtype: null 26 | tags: [] 27 | target: https://en.wikipedia.org/wiki/42_(number) 28 | type: macro.link 29 | - data: 30 | args: [] 31 | kwargs: {} 32 | subtype: null 33 | tags: [] 34 | type: text 35 | value: . 36 | kwargs: {} 37 | subtype: null 38 | tags: [] 39 | title: {} 40 | type: paragraph 41 | - data: 42 | args: [] 43 | content: 44 | - data: 45 | args: [] 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: text 50 | value: 'Booleans: and .' 51 | kwargs: {} 52 | subtype: null 53 | tags: [] 54 | title: {} 55 | type: paragraph 56 | - data: 57 | args: [] 58 | content: 59 | - data: 60 | args: [] 61 | kwargs: {} 62 | subtype: null 63 | tags: [] 64 | type: text 65 | value: 'Namespaces with a dotted syntax: 5 and 6.' 66 | kwargs: {} 67 | subtype: null 68 | tags: [] 69 | title: {} 70 | type: paragraph 71 | kwargs: {} 72 | subtype: null 73 | tags: [] 74 | type: document 75 | 76 | -------------------------------------------------------------------------------- /tests/e2e/expected/other/variable_simple.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: The Answer to the Ultimate Question of Life, the Universe, and Everything 14 | is 42. 15 | kwargs: {} 16 | subtype: null 17 | tags: [] 18 | title: {} 19 | type: paragraph 20 | - data: 21 | args: [] 22 | content: 23 | - data: 24 | args: [] 25 | kwargs: {} 26 | subtype: null 27 | tags: [] 28 | type: text 29 | value: 'Interpolation can be blocked escaping the curly braces: {answer}. 30 | This happens automatically in verbatim: ' 31 | - data: 32 | args: [] 33 | kwargs: {} 34 | subtype: null 35 | tags: [] 36 | type: verbatim 37 | value: '{answer}' 38 | - data: 39 | args: [] 40 | kwargs: {} 41 | subtype: null 42 | tags: [] 43 | type: text 44 | value: . 45 | kwargs: {} 46 | subtype: null 47 | tags: [] 48 | title: {} 49 | type: paragraph 50 | kwargs: {} 51 | subtype: null 52 | tags: [] 53 | type: document 54 | 55 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/comments.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: [] 4 | kwargs: {} 5 | subtype: null 6 | tags: [] 7 | type: document 8 | 9 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/header_multiple.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | anchor: header-1-1aa5 6 | args: [] 7 | kwargs: {} 8 | level: 1 9 | subtype: null 10 | tags: [] 11 | type: header 12 | value: 13 | data: 14 | args: [] 15 | content: 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: text 22 | value: Header 1 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: sentence 27 | - data: 28 | anchor: header-2-871b 29 | args: [] 30 | kwargs: {} 31 | level: 2 32 | subtype: null 33 | tags: [] 34 | type: header 35 | value: 36 | data: 37 | args: [] 38 | content: 39 | - data: 40 | args: [] 41 | kwargs: {} 42 | subtype: null 43 | tags: [] 44 | type: text 45 | value: Header 2 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: sentence 50 | - data: 51 | anchor: header-3-702d 52 | args: [] 53 | kwargs: {} 54 | level: 3 55 | subtype: null 56 | tags: [] 57 | type: header 58 | value: 59 | data: 60 | args: [] 61 | content: 62 | - data: 63 | args: [] 64 | kwargs: {} 65 | subtype: null 66 | tags: [] 67 | type: text 68 | value: Header 3 69 | kwargs: {} 70 | subtype: null 71 | tags: [] 72 | type: sentence 73 | kwargs: {} 74 | subtype: null 75 | tags: [] 76 | type: document 77 | 78 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/header_single.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | anchor: header-1-1aa5 6 | args: [] 7 | kwargs: {} 8 | level: 1 9 | subtype: null 10 | tags: [] 11 | type: header 12 | value: 13 | data: 14 | args: [] 15 | content: 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: text 22 | value: Header 1 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: sentence 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | type: document 31 | 32 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/header_with_paragraphs.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | anchor: header-1-1aa5 6 | args: [] 7 | kwargs: {} 8 | level: 1 9 | subtype: null 10 | tags: [] 11 | type: header 12 | value: 13 | data: 14 | args: [] 15 | content: 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: text 22 | value: Header 1 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | type: sentence 27 | - data: 28 | args: [] 29 | content: 30 | - data: 31 | args: [] 32 | kwargs: {} 33 | subtype: null 34 | tags: [] 35 | type: text 36 | value: Text under header 1 37 | kwargs: {} 38 | subtype: null 39 | tags: [] 40 | title: {} 41 | type: paragraph 42 | - data: 43 | anchor: header-2-871b 44 | args: [] 45 | kwargs: {} 46 | level: 2 47 | subtype: null 48 | tags: [] 49 | type: header 50 | value: 51 | data: 52 | args: [] 53 | content: 54 | - data: 55 | args: [] 56 | kwargs: {} 57 | subtype: null 58 | tags: [] 59 | type: text 60 | value: Header 2 61 | kwargs: {} 62 | subtype: null 63 | tags: [] 64 | type: sentence 65 | - data: 66 | args: [] 67 | content: 68 | - data: 69 | args: [] 70 | kwargs: {} 71 | subtype: null 72 | tags: [] 73 | type: text 74 | value: Text under header 2 75 | kwargs: {} 76 | subtype: null 77 | tags: [] 78 | title: {} 79 | type: paragraph 80 | - data: 81 | anchor: header-3-702d 82 | args: [] 83 | kwargs: {} 84 | level: 3 85 | subtype: null 86 | tags: [] 87 | type: header 88 | value: 89 | data: 90 | args: [] 91 | content: 92 | - data: 93 | args: [] 94 | kwargs: {} 95 | subtype: null 96 | tags: [] 97 | type: text 98 | value: Header 3 99 | kwargs: {} 100 | subtype: null 101 | tags: [] 102 | type: sentence 103 | - data: 104 | args: [] 105 | content: 106 | - data: 107 | args: [] 108 | kwargs: {} 109 | subtype: null 110 | tags: [] 111 | type: text 112 | value: Text under header 3 113 | kwargs: {} 114 | subtype: null 115 | tags: [] 116 | title: {} 117 | type: paragraph 118 | kwargs: {} 119 | subtype: null 120 | tags: [] 121 | type: document 122 | 123 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/horizontal_line.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: Under this paragraph there is a horizontal line. 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | - data: 20 | args: [] 21 | kwargs: {} 22 | subtype: null 23 | tags: [] 24 | type: horizontal_rule 25 | - data: 26 | args: [] 27 | content: 28 | - data: 29 | args: [] 30 | kwargs: {} 31 | subtype: null 32 | tags: [] 33 | type: text 34 | value: Above this paragraph there is a horizontal line. 35 | kwargs: {} 36 | subtype: null 37 | tags: [] 38 | title: {} 39 | type: paragraph 40 | kwargs: {} 41 | subtype: null 42 | tags: [] 43 | type: document 44 | 45 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/image.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This includes an image with a caption 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | - data: 20 | alt_text: null 21 | args: [] 22 | classes: null 23 | kwargs: {} 24 | subtype: null 25 | tags: [] 26 | title: 27 | data: 28 | args: [] 29 | content: 30 | - data: 31 | args: [] 32 | kwargs: {} 33 | subtype: null 34 | tags: [] 35 | type: text 36 | value: A beautiful image 37 | kwargs: {} 38 | subtype: null 39 | tags: [] 40 | type: sentence 41 | type: content_image 42 | uri: https://images.unsplash.com/photo-1534106474077-f9e9c6f5a47c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1740&q=80 43 | kwargs: {} 44 | subtype: null 45 | tags: [] 46 | type: document 47 | 48 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/list_ordered.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This is an ordered list 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | - data: 20 | args: [] 21 | items: 22 | - data: 23 | args: [] 24 | content: 25 | - data: 26 | args: [] 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | type: text 31 | value: Item 1 32 | kwargs: {} 33 | level: 1 34 | subtype: null 35 | tags: [] 36 | type: list_item 37 | - data: 38 | args: [] 39 | content: 40 | - data: 41 | args: [] 42 | kwargs: {} 43 | subtype: null 44 | tags: [] 45 | type: text 46 | value: Item 2 47 | - data: 48 | args: [] 49 | items: 50 | - data: 51 | args: [] 52 | content: 53 | - data: 54 | args: [] 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: text 59 | value: Item 2.1 60 | kwargs: {} 61 | level: 2 62 | subtype: null 63 | tags: [] 64 | type: list_item 65 | - data: 66 | args: [] 67 | content: 68 | - data: 69 | args: [] 70 | kwargs: {} 71 | subtype: null 72 | tags: [] 73 | type: text 74 | value: Item 2.2 75 | kwargs: {} 76 | level: 2 77 | subtype: null 78 | tags: [] 79 | type: list_item 80 | kwargs: {} 81 | main_node: false 82 | ordered: true 83 | start: 1 84 | subtype: null 85 | tags: [] 86 | type: list 87 | kwargs: {} 88 | level: 1 89 | subtype: null 90 | tags: [] 91 | type: list_item 92 | - data: 93 | args: [] 94 | content: 95 | - data: 96 | args: [] 97 | kwargs: {} 98 | subtype: null 99 | tags: [] 100 | type: text 101 | value: Item 3 102 | kwargs: {} 103 | level: 1 104 | subtype: null 105 | tags: [] 106 | type: list_item 107 | kwargs: {} 108 | main_node: true 109 | ordered: true 110 | start: 1 111 | subtype: null 112 | tags: [] 113 | type: list 114 | kwargs: {} 115 | subtype: null 116 | tags: [] 117 | type: document 118 | 119 | -------------------------------------------------------------------------------- /tests/e2e/expected/page/list_unordered.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | content: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | subtype: null 11 | tags: [] 12 | type: text 13 | value: This is an unordered list 14 | kwargs: {} 15 | subtype: null 16 | tags: [] 17 | title: {} 18 | type: paragraph 19 | - data: 20 | args: [] 21 | items: 22 | - data: 23 | args: [] 24 | content: 25 | - data: 26 | args: [] 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | type: text 31 | value: Item 1 32 | kwargs: {} 33 | level: 1 34 | subtype: null 35 | tags: [] 36 | type: list_item 37 | - data: 38 | args: [] 39 | content: 40 | - data: 41 | args: [] 42 | kwargs: {} 43 | subtype: null 44 | tags: [] 45 | type: text 46 | value: Item 2 47 | - data: 48 | args: [] 49 | items: 50 | - data: 51 | args: [] 52 | content: 53 | - data: 54 | args: [] 55 | kwargs: {} 56 | subtype: null 57 | tags: [] 58 | type: text 59 | value: Item 2.1 60 | kwargs: {} 61 | level: 2 62 | subtype: null 63 | tags: [] 64 | type: list_item 65 | - data: 66 | args: [] 67 | content: 68 | - data: 69 | args: [] 70 | kwargs: {} 71 | subtype: null 72 | tags: [] 73 | type: text 74 | value: Item 2.2 75 | kwargs: {} 76 | level: 2 77 | subtype: null 78 | tags: [] 79 | type: list_item 80 | kwargs: {} 81 | main_node: false 82 | ordered: false 83 | start: 1 84 | subtype: null 85 | tags: [] 86 | type: list 87 | kwargs: {} 88 | level: 1 89 | subtype: null 90 | tags: [] 91 | type: list_item 92 | - data: 93 | args: [] 94 | content: 95 | - data: 96 | args: [] 97 | kwargs: {} 98 | subtype: null 99 | tags: [] 100 | type: text 101 | value: Item 3 102 | kwargs: {} 103 | level: 1 104 | subtype: null 105 | tags: [] 106 | type: list_item 107 | kwargs: {} 108 | main_node: true 109 | ordered: false 110 | start: 1 111 | subtype: null 112 | tags: [] 113 | type: list 114 | kwargs: {} 115 | subtype: null 116 | tags: [] 117 | type: document 118 | 119 | -------------------------------------------------------------------------------- /tests/e2e/expected/source/source_callouts.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | callouts: 7 | - data: 8 | args: [] 9 | kwargs: {} 10 | marker: '1' 11 | subtype: null 12 | tags: [] 13 | type: callouts_entry 14 | value: The name of the function 15 | - data: 16 | args: [] 17 | kwargs: {} 18 | marker: '2' 19 | subtype: null 20 | tags: [] 21 | type: callouts_entry 22 | value: Some memory-related wizardry 23 | classes: [] 24 | code: 25 | - data: 26 | args: [] 27 | kwargs: {} 28 | subtype: null 29 | tags: [] 30 | type: raw 31 | value: 'def header_anchor(text, level):' 32 | - data: 33 | args: [] 34 | kwargs: {} 35 | subtype: null 36 | tags: [] 37 | type: raw 38 | value: ' return "h{}-{}-{}".format(' 39 | - data: 40 | args: [] 41 | kwargs: {} 42 | subtype: null 43 | tags: [] 44 | type: raw 45 | value: ' level, quote(text.lower())[:20], str(id(text))[:8]' 46 | - data: 47 | args: [] 48 | kwargs: {} 49 | subtype: null 50 | tags: [] 51 | type: raw 52 | value: ' ) # pragma: no cover' 53 | highlights: [] 54 | kwargs: {} 55 | language: python 56 | lines: 4 57 | markers: 58 | - data: 59 | args: [] 60 | kwargs: {} 61 | line: 0 62 | marker: '1' 63 | subtype: null 64 | tags: [] 65 | type: callout 66 | - null 67 | - data: 68 | args: [] 69 | kwargs: {} 70 | line: 2 71 | marker: '2' 72 | subtype: null 73 | tags: [] 74 | type: callout 75 | - null 76 | preprocessor: null 77 | subtype: null 78 | tags: [] 79 | title: {} 80 | type: source 81 | kwargs: {} 82 | subtype: null 83 | tags: [] 84 | type: document 85 | 86 | -------------------------------------------------------------------------------- /tests/e2e/expected/source/source_simple.yaml: -------------------------------------------------------------------------------- 1 | data: 2 | args: [] 3 | content: 4 | - data: 5 | args: [] 6 | callouts: [] 7 | classes: [] 8 | code: 9 | - data: 10 | args: [] 11 | kwargs: {} 12 | subtype: null 13 | tags: [] 14 | type: raw 15 | value: This is all literal. 16 | - data: 17 | args: [] 18 | kwargs: {} 19 | subtype: null 20 | tags: [] 21 | type: raw 22 | value: '' 23 | - data: 24 | args: [] 25 | kwargs: {} 26 | subtype: null 27 | tags: [] 28 | type: raw 29 | value: == This is not a header 30 | - data: 31 | args: [] 32 | kwargs: {} 33 | subtype: null 34 | tags: [] 35 | type: raw 36 | value: '' 37 | - data: 38 | args: [] 39 | kwargs: {} 40 | subtype: null 41 | tags: [] 42 | type: raw 43 | value: '[These are not attributes]' 44 | - data: 45 | args: [] 46 | kwargs: {} 47 | subtype: null 48 | tags: [] 49 | type: raw 50 | value: '' 51 | - data: 52 | args: [] 53 | kwargs: {} 54 | subtype: null 55 | tags: [] 56 | type: raw 57 | value: Some quotes "" 58 | highlights: [] 59 | kwargs: {} 60 | language: text 61 | lines: 7 62 | markers: 63 | - null 64 | - null 65 | - null 66 | - null 67 | - null 68 | - null 69 | - null 70 | preprocessor: null 71 | subtype: null 72 | tags: [] 73 | title: 74 | data: 75 | args: [] 76 | content: 77 | - data: 78 | args: [] 79 | kwargs: {} 80 | subtype: null 81 | tags: [] 82 | type: text 83 | value: Source code 84 | kwargs: {} 85 | subtype: null 86 | tags: [] 87 | type: sentence 88 | type: source 89 | kwargs: {} 90 | subtype: null 91 | tags: [] 92 | type: document 93 | 94 | -------------------------------------------------------------------------------- /tests/e2e/source/block/block_admonition.mau: -------------------------------------------------------------------------------- 1 | [*admonition, class="warning", icon="someicon"] 2 | .somelabel 3 | ---- 4 | This is an admonition, which is a specific type of block with a `class`, and `icon`, and a `label` that work as a title. This block is implemented purely through Jinja templates. 5 | ---- 6 | -------------------------------------------------------------------------------- /tests/e2e/source/block/block_conditionals.mau: -------------------------------------------------------------------------------- 1 | :render:yes 2 | 3 | @if:render:=yes 4 | [*aside] 5 | ---- 6 | This will be rendered 7 | ---- 8 | 9 | @if:render:=no 10 | [*aside] 11 | ---- 12 | This will not be rendered 13 | ---- 14 | -------------------------------------------------------------------------------- /tests/e2e/source/block/block_quote.mau: -------------------------------------------------------------------------------- 1 | [*quote] 2 | ---- 3 | Some of the newer ones are having trouble because they never really mastered some basic techniques, but they’re working hard and improving. 4 | ---- 5 | Orson Scott Card, _Ender's Game_ (1985) 6 | -------------------------------------------------------------------------------- /tests/e2e/source/block/block_simple.mau: -------------------------------------------------------------------------------- 1 | ---- 2 | This is a block without attributes. It should be rendered as a div, however. 3 | ---- 4 | 5 | ++++ 6 | This is another block between two markers `++++`. 7 | ++++ 8 | 9 | The four characters can be escaped 10 | 11 | \---- 12 | 13 | [*sometype] 14 | ---- 15 | A block with a subtype 16 | ---- 17 | -------------------------------------------------------------------------------- /tests/e2e/source/block/block_title.mau: -------------------------------------------------------------------------------- 1 | . The title 2 | ---- 3 | This is a block 4 | ---- 5 | -------------------------------------------------------------------------------- /tests/e2e/source/commands/footnotes.mau: -------------------------------------------------------------------------------- 1 | This file contains a footnote[footnote](note) 2 | 3 | [*footnote, note] 4 | ---- 5 | This is a footnote 6 | ---- 7 | 8 | ::footnotes: 9 | -------------------------------------------------------------------------------- /tests/e2e/source/commands/footnotes_advanced.mau: -------------------------------------------------------------------------------- 1 | = Test footnotes 2 | 3 | = Header 1 4 | 5 | [*footnote, intended] 6 | ---- 7 | Which means that links work and that I can add the command `footnotes` anywhere in the text. 8 | ---- 9 | 10 | This file tests that footnotes work as intended[footnote](intended) 11 | 12 | == Header 2 13 | 14 | The list of footnotes should be computed while the parser works on the text, so I expect the command `footnotes` to contain all of them in any part of the text[footnote](hope) 15 | 16 | ::footnotes: 17 | 18 | [*footnote, hope] 19 | ---- 20 | This is my hope, at least. 21 | ---- 22 | 23 | === Header 3 24 | 25 | This is here just to add another footnote[footnote](appear). 26 | 27 | [*footnote, appear] 28 | ---- 29 | Which, as I said, should appear both in the list above and in the one below. 30 | ---- 31 | 32 | ::footnotes: 33 | -------------------------------------------------------------------------------- /tests/e2e/source/commands/toc.mau: -------------------------------------------------------------------------------- 1 | = Test ToC 2 | 3 | ::toc: 4 | 5 | -------------------------------------------------------------------------------- /tests/e2e/source/commands/toc_advanced.mau: -------------------------------------------------------------------------------- 1 | = Test ToC 2 | 3 | == Works as intended 4 | 5 | This file tests that headers work as intended. 6 | 7 | === More on the ToC 8 | 9 | The ToC is the list of all headers in the file, and it should be possible to include it in any part of it. Like here 10 | 11 | ::toc: 12 | 13 | == Conclusion 14 | 15 | This is another example, the ToC here should be the same as the ToC above. 16 | 17 | ::toc: 18 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/classes.mau: -------------------------------------------------------------------------------- 1 | This paragraph uses [class]("classes", class1, class2). 2 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/conditionals.mau: -------------------------------------------------------------------------------- 1 | :+true: 2 | :-false: 3 | 4 | A true statement is [@if:true:&true]("TRUE", "FALSE"). 5 | 6 | A false statement is [@if:false:&true]("TRUE", "FALSE"). 7 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/inline_image.mau: -------------------------------------------------------------------------------- 1 | A paragraph with an inline image [image]("https://www.thedigitalcatonline.com/images/global/favicon.jpg"). 2 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/internal_link.mau: -------------------------------------------------------------------------------- 1 | This is an [header](someid, "internal link"). 2 | 3 | [id=someid] 4 | = Header 5 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/link.mau: -------------------------------------------------------------------------------- 1 | Links like there was no [link]("https://www.thedigitalcatonline.com", "tomorrow"). 2 | 3 | Also without text: [link]("https://www.thedigitalcatonline.com"). 4 | 5 | Link text should be parsed [link]("https://example.com", text="*tomorrow*"). 6 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/mailto.mau: -------------------------------------------------------------------------------- 1 | You can use the macro `mailto` to format email links. If you need to know more write me at [mailto]("info@example.com"). 2 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/multiple_paragraphs.mau: -------------------------------------------------------------------------------- 1 | This document contains multiple paragraphs. 2 | 3 | They are separated by an empty line. 4 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/single_paragraph.mau: -------------------------------------------------------------------------------- 1 | This document contains a single paragraph. 2 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/split_paragraph.mau: -------------------------------------------------------------------------------- 1 | This is a paragraph split in two lines. 2 | The missing empty line makes the two line belong to the same paragraph. 3 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/styles.mau: -------------------------------------------------------------------------------- 1 | This paragraph contains _underscore_ text. 2 | 3 | This paragraph contains *star* text. 4 | 5 | This paragraph contains ~tilde~ text. 6 | 7 | This paragraph contains ^caret^ text. 8 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/styles_mixed.mau: -------------------------------------------------------------------------------- 1 | You can have _*strong and empashized*_ text. 2 | 3 | You can also apply styles to _*`verbatim`*_. 4 | 5 | But verbatim will `_*preserve*_` them. 6 | 7 | You can use _single *markers. 8 | 9 | But you \_need\_ to escape pairs. 10 | 11 | Even though you can escape \_only one_ of the two. 12 | 13 | If you have \_more than two\_ it's better to just \_escape\_ all of them. 14 | 15 | Oh, this is valid for `verbatim as well. 16 | -------------------------------------------------------------------------------- /tests/e2e/source/inline/verbatim.mau: -------------------------------------------------------------------------------- 1 | This paragraph contains `verbatim` text. 2 | -------------------------------------------------------------------------------- /tests/e2e/source/other/directives.mau: -------------------------------------------------------------------------------- 1 | = Test directives 2 | 3 | This file tests Mau directives 4 | 5 | ::#include:tests/e2e/source/other/include_text.txt 6 | -------------------------------------------------------------------------------- /tests/e2e/source/other/include_text.txt: -------------------------------------------------------------------------------- 1 | This text has been included through a directive 2 | -------------------------------------------------------------------------------- /tests/e2e/source/other/variable_advanced.mau: -------------------------------------------------------------------------------- 1 | :wikipedia_link:[link]("https://en.wikipedia.org/wiki/42_(number)") 2 | 3 | A variable can contain Mau code: {wikipedia_link}. 4 | 5 | :+true: 6 | :-false: 7 | 8 | Booleans: {true} and {false}. 9 | 10 | :value:5 11 | :module.value:6 12 | 13 | Namespaces with a dotted syntax: {value} and {module.value}. 14 | -------------------------------------------------------------------------------- /tests/e2e/source/other/variable_simple.mau: -------------------------------------------------------------------------------- 1 | :answer:42 2 | 3 | The Answer to the Ultimate Question of Life, the Universe, and Everything is {answer}. 4 | 5 | Interpolation can be blocked escaping the curly braces: \{answer}. This happens automatically in verbatim: `{answer}`. 6 | -------------------------------------------------------------------------------- /tests/e2e/source/page/comments.mau: -------------------------------------------------------------------------------- 1 | // This is a comment and will not appear in the output. 2 | 3 | //// 4 | This is a multiline 5 | comment. 6 | All these lines will disappear. 7 | //// 8 | 9 | -------------------------------------------------------------------------------- /tests/e2e/source/page/header_multiple.mau: -------------------------------------------------------------------------------- 1 | = Header 1 2 | 3 | == Header 2 4 | 5 | === Header 3 6 | -------------------------------------------------------------------------------- /tests/e2e/source/page/header_single.mau: -------------------------------------------------------------------------------- 1 | = Header 1 2 | -------------------------------------------------------------------------------- /tests/e2e/source/page/header_with_paragraphs.mau: -------------------------------------------------------------------------------- 1 | = Header 1 2 | 3 | Text under header 1 4 | 5 | == Header 2 6 | 7 | Text under header 2 8 | 9 | === Header 3 10 | 11 | Text under header 3 12 | -------------------------------------------------------------------------------- /tests/e2e/source/page/horizontal_line.mau: -------------------------------------------------------------------------------- 1 | Under this paragraph there is a horizontal line. 2 | 3 | --- 4 | 5 | Above this paragraph there is a horizontal line. 6 | -------------------------------------------------------------------------------- /tests/e2e/source/page/image.mau: -------------------------------------------------------------------------------- 1 | This includes an image with a caption 2 | 3 | .A beautiful image 4 | << image:"https://images.unsplash.com/photo-1534106474077-f9e9c6f5a47c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1740&q=80" 5 | -------------------------------------------------------------------------------- /tests/e2e/source/page/list_ordered.mau: -------------------------------------------------------------------------------- 1 | This is an ordered list 2 | 3 | # Item 1 4 | # Item 2 5 | ## Item 2.1 6 | ## Item 2.2 7 | # Item 3 8 | -------------------------------------------------------------------------------- /tests/e2e/source/page/list_start.mau: -------------------------------------------------------------------------------- 1 | This is an ordered list that starts at 5 2 | 3 | [start=5] 4 | # Item 5 5 | # Item 6 6 | ## Item 6.1 7 | ## Item 6.2 8 | # Item 7 9 | 10 | This is an ordered list that starts at 8 automatically 11 | 12 | [start=auto] 13 | # Item 8 14 | # Item 9 15 | # Item 10 16 | 17 | Unordered lists ignore the start 18 | 19 | [start=42] 20 | * An item 21 | * Another item 22 | * A third item 23 | -------------------------------------------------------------------------------- /tests/e2e/source/page/list_unordered.mau: -------------------------------------------------------------------------------- 1 | This is an unordered list 2 | 3 | * Item 1 4 | * Item 2 5 | ** Item 2.1 6 | ** Item 2.2 7 | * Item 3 8 | -------------------------------------------------------------------------------- /tests/e2e/source/source/source_callouts.mau: -------------------------------------------------------------------------------- 1 | [*source,python] 2 | ---- 3 | def header_anchor(text, level)::1: 4 | return "h{}-{}-{}".format( 5 | level, quote(text.lower())[:20], str(id(text))[:8]:2: 6 | ) # pragma: no cover 7 | ---- 8 | 1: The name of the function 9 | 2: Some memory-related wizardry 10 | -------------------------------------------------------------------------------- /tests/e2e/source/source/source_highlight.mau: -------------------------------------------------------------------------------- 1 | . Python code 2 | [*source, python] 3 | ---- 4 | def header_anchor(text, level): 5 | """ 6 | Return a sanitised anchor for a header. 7 | """ 8 | 9 | # Everything lowercase 10 | sanitised_text = text.lower() 11 | 12 | # Get only letters, numbers, dashes, spaces, and dots 13 | sanitised_text = "".join(re.findall("[a-z0-9-\\. ]+", sanitised_text)) 14 | 15 | # Remove multiple spaces 16 | sanitised_text = "-".join(sanitised_text.split()) 17 | 18 | return sanitised_text 19 | ---- 20 | -------------------------------------------------------------------------------- /tests/e2e/source/source/source_simple.mau: -------------------------------------------------------------------------------- 1 | . Source code 2 | [*source] 3 | ---- 4 | This is all literal. 5 | 6 | == This is not a header 7 | 8 | [These are not attributes] 9 | 10 | Some quotes "" 11 | ---- 12 | -------------------------------------------------------------------------------- /tests/e2e/test_e2e.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pytest 3 | import yaml 4 | 5 | from mau.environment.environment import Environment 6 | from mau.nodes.page import DocumentNode 7 | from mau.visitors.base_visitor import BaseVisitor 8 | from mau.lexers.main_lexer import MainLexer 9 | from mau.parsers.main_parser import MainParser 10 | from mau.test_helpers import ( 11 | init_parser_factory, 12 | parser_runner_factory, 13 | collect_test_files, 14 | ) 15 | 16 | init_parser = init_parser_factory(MainLexer, MainParser) 17 | 18 | runner = parser_runner_factory(MainLexer, MainParser) 19 | 20 | tests_dir = pathlib.Path(__file__).parent 21 | 22 | tst_files = collect_test_files(tests_dir, "source", "mau", "expected", "yaml") 23 | 24 | 25 | @pytest.mark.parametrize("source,expected", tst_files) 26 | def test_e2e(source, expected): 27 | with open(source, encoding="utf-8") as source_file: 28 | source_code = source_file.read() 29 | 30 | with open(expected, encoding="utf-8") as expected_file: 31 | expected_code = yaml.load(expected_file.read(), Loader=yaml.FullLoader) 32 | 33 | parser = runner(source_code) 34 | 35 | node = DocumentNode(children=parser.nodes) 36 | visitor = BaseVisitor(Environment()) 37 | result = visitor.visit(node) 38 | 39 | assert result == expected_code 40 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from mau.test_helpers import * 2 | -------------------------------------------------------------------------------- /tests/lexers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/lexers/__init__.py -------------------------------------------------------------------------------- /tests/lexers/data_file_lexer.txt: -------------------------------------------------------------------------------- 1 | @BEGIN 2 | This is text 3 | split into multiple lines 4 | 5 | with an empty line 6 | --- 7 | [attributes] 8 | ---- 9 | This is a block 10 | #### 11 | That contains another block 12 | #### 13 | ---- 14 | @@@ 15 | main|0|0|TEXT|This is text 16 | main|1|0|TEXT|split into multiple lines 17 | main|2|0|EOL| 18 | main|3|0|TEXT|with an empty line 19 | main|4|0|HORIZONTAL_RULE| 20 | main|5|0|ATTRIBUTES|[attributes] 21 | main|6|0|BLOCK|---- 22 | main|7|0|TEXT|This is a block 23 | main|8|0|BLOCK|#### 24 | main|9|0|TEXT|That contains another block 25 | main|10|0|BLOCK|#### 26 | main|11|0|BLOCK|---- 27 | @END 28 | -------------------------------------------------------------------------------- /tests/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/nodes/__init__.py -------------------------------------------------------------------------------- /tests/nodes/test_arguments.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.arguments import NamedArgumentNode, UnnamedArgumentNode 2 | 3 | 4 | def test_unnamed_argument_node_value(): 5 | node = UnnamedArgumentNode("somevalue") 6 | 7 | assert node.value == "somevalue" 8 | assert node.node_type == "unnamed_argument" 9 | assert node == UnnamedArgumentNode("somevalue") 10 | 11 | 12 | def test_named_argument_node_value(): 13 | node = NamedArgumentNode("somekey", "somevalue") 14 | 15 | assert node.key == "somekey" 16 | assert node.value == "somevalue" 17 | assert node.node_type == "named_argument" 18 | assert node == NamedArgumentNode("somekey", "somevalue") 19 | -------------------------------------------------------------------------------- /tests/nodes/test_block.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.block import BlockNode 2 | 3 | 4 | def test_block_node(): 5 | node = BlockNode(subtype="sometype") 6 | 7 | assert node.subtype == "sometype" 8 | assert node.children == [] 9 | assert node.secondary_children == [] 10 | assert node.args == [] 11 | assert node.kwargs == {} 12 | assert node.node_type == "block" 13 | assert node == BlockNode(subtype="sometype") 14 | -------------------------------------------------------------------------------- /tests/nodes/test_content.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.content import ContentImageNode, ContentNode 2 | 3 | 4 | def test_content_node(): 5 | node = ContentNode( 6 | content_type="somecontent", uris=["/uri1", "/uri2"], title="sometitle" 7 | ) 8 | 9 | assert node.content_type == "somecontent" 10 | assert node.uris == ["/uri1", "/uri2"] 11 | assert node.title == "sometitle" 12 | assert node.args == [] 13 | assert node.kwargs == {} 14 | assert node.node_type == "content" 15 | assert node == ContentNode( 16 | content_type="somecontent", uris=["/uri1", "/uri2"], title="sometitle" 17 | ) 18 | 19 | 20 | def test_content_image_node(): 21 | node = ContentImageNode( 22 | uri="someuri", alt_text="somealttext", classes=[], title="sometitle" 23 | ) 24 | 25 | assert node.uri == "someuri" 26 | assert node.alt_text == "somealttext" 27 | assert node.classes == [] 28 | assert node.title == "sometitle" 29 | assert node.args == [] 30 | assert node.kwargs == {} 31 | assert node.node_type == "content_image" 32 | assert node == ContentImageNode( 33 | uri="someuri", alt_text="somealttext", classes=[], title="sometitle" 34 | ) 35 | -------------------------------------------------------------------------------- /tests/nodes/test_footnotes.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.footnotes import FootnoteNode, FootnotesNode 2 | from mau.nodes.inline import TextNode, VerbatimNode 3 | 4 | 5 | def test_footnote_node(): 6 | node = FootnoteNode(children=[VerbatimNode("somevalue"), TextNode("othervalue")]) 7 | 8 | assert node.children == [VerbatimNode("somevalue"), TextNode("othervalue")] 9 | assert node.number is None 10 | assert node.reference_anchor is None 11 | assert node.content_anchor is None 12 | assert node.node_type == "footnote" 13 | assert node == FootnoteNode( 14 | children=[VerbatimNode("somevalue"), TextNode("othervalue")] 15 | ) 16 | 17 | 18 | def test_footnote_node_with_number_and_anchor(): 19 | node = FootnoteNode( 20 | children=[VerbatimNode("somevalue"), TextNode("othervalue")], 21 | number="3", 22 | reference_anchor="someanchor", 23 | content_anchor="someanchor-def", 24 | ) 25 | 26 | assert node.children == [VerbatimNode("somevalue"), TextNode("othervalue")] 27 | assert node.number == "3" 28 | assert node.reference_anchor == "someanchor" 29 | assert node.content_anchor == "someanchor-def" 30 | assert node.node_type == "footnote" 31 | assert node == FootnoteNode( 32 | children=[VerbatimNode("somevalue"), TextNode("othervalue")], 33 | number="3", 34 | reference_anchor="someanchor", 35 | content_anchor="someanchor-def", 36 | ) 37 | 38 | 39 | def test_footnotes_entry_node(): 40 | node = FootnoteNode( 41 | children=[VerbatimNode("somevalue"), TextNode("othervalue")] 42 | ).to_entry() 43 | 44 | assert node.node_type == "footnotes_entry" 45 | 46 | 47 | def test_footnotes_node(): 48 | node = FootnotesNode( 49 | children=[TextNode("somevalue1"), TextNode("somevalue2")], 50 | args=["value1", "value2"], 51 | tags=["tag1", "tag2"], 52 | kwargs={"key1": "text1", "key2": "text2"}, 53 | ) 54 | 55 | assert node.children == [TextNode("somevalue1"), TextNode("somevalue2")] 56 | assert node.args == ["value1", "value2"] 57 | assert node.tags == ["tag1", "tag2"] 58 | assert node.kwargs == {"key1": "text1", "key2": "text2"} 59 | assert node.node_type == "footnotes" 60 | assert node == FootnotesNode( 61 | children=[TextNode("somevalue1"), TextNode("somevalue2")], 62 | args=["value1", "value2"], 63 | tags=["tag1", "tag2"], 64 | kwargs={"key1": "text1", "key2": "text2"}, 65 | ) 66 | -------------------------------------------------------------------------------- /tests/nodes/test_header.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.header import HeaderNode 2 | 3 | 4 | def test_header_node(): 5 | node = HeaderNode( 6 | "somevalue", 7 | "somelevel", 8 | "someanchor", 9 | args=["value1", "value2"], 10 | tags=["tag1", "tag2"], 11 | kwargs={"key1": "text1", "key2": "text2"}, 12 | ) 13 | 14 | assert node.value == "somevalue" 15 | assert node.level == "somelevel" 16 | assert node.anchor == "someanchor" 17 | assert node.args == ["value1", "value2"] 18 | assert node.tags == ["tag1", "tag2"] 19 | assert node.kwargs == {"key1": "text1", "key2": "text2"} 20 | assert node.node_type == "header" 21 | assert node == HeaderNode( 22 | "somevalue", 23 | "somelevel", 24 | "someanchor", 25 | args=["value1", "value2"], 26 | tags=["tag1", "tag2"], 27 | kwargs={"key1": "text1", "key2": "text2"}, 28 | ) 29 | -------------------------------------------------------------------------------- /tests/nodes/test_inline.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.inline import RawNode, StyleNode, TextNode, VerbatimNode, WordNode 2 | 3 | 4 | def test_word_node(): 5 | node = WordNode("somevalue") 6 | 7 | assert node.value == "somevalue" 8 | assert node.node_type == "word" 9 | assert node == WordNode("somevalue") 10 | 11 | 12 | def test_text_node(): 13 | node = TextNode("somevalue") 14 | 15 | assert node.value == "somevalue" 16 | assert node.node_type == "text" 17 | assert node == TextNode("somevalue") 18 | 19 | 20 | def test_raw_node(): 21 | node = RawNode("somevalue") 22 | 23 | assert node.value == "somevalue" 24 | assert node.node_type == "raw" 25 | assert node == RawNode("somevalue") 26 | 27 | 28 | def test_verbatim_node(): 29 | node = VerbatimNode("somevalue") 30 | 31 | assert node.value == "somevalue" 32 | assert node.node_type == "verbatim" 33 | assert node == VerbatimNode("somevalue") 34 | 35 | 36 | def test_style_node(): 37 | node = StyleNode("mystyle", children=[TextNode("othervalue")]) 38 | 39 | assert node.value == "mystyle" 40 | assert node.children == [TextNode("othervalue")] 41 | assert node.node_type == "style" 42 | assert node == StyleNode("mystyle", children=[TextNode("othervalue")]) 43 | -------------------------------------------------------------------------------- /tests/nodes/test_lists.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.inline import TextNode 2 | from mau.nodes.lists import ListItemNode, ListNode 3 | 4 | 5 | def test_list_item_node(): 6 | node = ListItemNode( 7 | level="3", children=[TextNode("This is a list with one element")] 8 | ) 9 | 10 | assert node.level == "3" 11 | assert node.children == [TextNode("This is a list with one element")] 12 | assert node.node_type == "list_item" 13 | assert node == ListItemNode( 14 | level="3", children=[TextNode("This is a list with one element")] 15 | ) 16 | 17 | 18 | def test_list_node(): 19 | node = ListNode(ordered=True, main_node=False) 20 | 21 | assert node.ordered is True 22 | assert node.children == [] 23 | assert node.main_node is False 24 | assert node.start == 1 25 | assert node.subtype is None 26 | assert node.args == [] 27 | assert node.kwargs == {} 28 | assert node.node_type == "list" 29 | assert node == ListNode(ordered=True, main_node=False) 30 | 31 | 32 | def test_list_node_with_start(): 33 | node = ListNode(ordered=True, main_node=False, start=42) 34 | 35 | assert node.ordered is True 36 | assert node.children == [] 37 | assert node.main_node is False 38 | assert node.start == 42 39 | assert node.subtype is None 40 | assert node.args == [] 41 | assert node.kwargs == {} 42 | assert node.node_type == "list" 43 | assert node == ListNode(ordered=True, main_node=False, start=42) 44 | -------------------------------------------------------------------------------- /tests/nodes/test_macros.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.header import HeaderNode 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.macros import ( 4 | MacroClassNode, 5 | MacroHeaderNode, 6 | MacroImageNode, 7 | MacroLinkNode, 8 | MacroNode, 9 | ) 10 | 11 | 12 | def test_macro_node(): 13 | node = MacroNode("aname", args=["value"], kwargs={"akey": "avalue"}) 14 | 15 | assert node.name == "aname" 16 | assert node.args == ["value"] 17 | assert node.kwargs == {"akey": "avalue"} 18 | assert node.node_type == "macro" 19 | assert node == MacroNode("aname", args=["value"], kwargs={"akey": "avalue"}) 20 | 21 | 22 | def test_class_node(): 23 | node = MacroClassNode(["class1", "class2"], children=[TextNode("othervalue")]) 24 | 25 | assert node.classes == ["class1", "class2"] 26 | assert node.children == [TextNode("othervalue")] 27 | assert node.node_type == "macro.class" 28 | assert node == MacroClassNode( 29 | ["class1", "class2"], children=[TextNode("othervalue")] 30 | ) 31 | 32 | 33 | def test_link_node(): 34 | node = MacroLinkNode("atarget", children=[TextNode("sometext")]) 35 | 36 | assert node.target == "atarget" 37 | assert node.children == [TextNode("sometext")] 38 | assert node.node_type == "macro.link" 39 | assert node == MacroLinkNode("atarget", children=[TextNode("sometext")]) 40 | 41 | 42 | def test_link_node_no_text(): 43 | node = MacroLinkNode("atarget") 44 | 45 | assert node.target == "atarget" 46 | assert node.children == [] 47 | assert node.node_type == "macro.link" 48 | assert node == MacroLinkNode("atarget") 49 | 50 | 51 | def test_image_node(): 52 | node = MacroImageNode("someuri", "somealttext", "width", "height") 53 | 54 | assert node.uri == "someuri" 55 | assert node.alt_text == "somealttext" 56 | assert node.width == "width" 57 | assert node.height == "height" 58 | assert node.node_type == "macro.image" 59 | assert node == MacroImageNode("someuri", "somealttext", "width", "height") 60 | 61 | 62 | def test_header_node(): 63 | header_node = HeaderNode("someheader", "1", "someanchor") 64 | node = MacroHeaderNode("someid", header_node, children=[TextNode("sometext")]) 65 | 66 | assert node.header_id == "someid" 67 | assert node.header == header_node 68 | assert node.children == [TextNode("sometext")] 69 | assert node.node_type == "macro.header" 70 | assert node == MacroHeaderNode( 71 | "someid", header_node, children=[TextNode("sometext")] 72 | ) 73 | -------------------------------------------------------------------------------- /tests/nodes/test_nodes.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from mau.nodes.nodes import Node, ValueNode 4 | 5 | 6 | def test_node(): 7 | node = Node() 8 | 9 | assert node.node_type == "node" 10 | assert node.asdict() == { 11 | "type": "node", 12 | "subtype": None, 13 | "children": [], 14 | "args": [], 15 | "kwargs": {}, 16 | "tags": [], 17 | } 18 | 19 | 20 | def test_value_node(): 21 | node = ValueNode("somevalue") 22 | 23 | assert node.value == "somevalue" 24 | assert node.node_type == "value_node" 25 | assert node == ValueNode("somevalue") 26 | 27 | 28 | def test_node_accept(): 29 | visitor = Mock() 30 | node = Node() 31 | node.node_type = "mynode" 32 | 33 | assert node.accept(visitor) == visitor._visit_mynode() 34 | 35 | 36 | def test_node_accept_default(): 37 | # This makes the Mock raise an AttributeError 38 | # for anything that is not in the list 39 | visitor = Mock(spec=["_visit_default"]) 40 | node = Node() 41 | node.node_type = "mynode" 42 | 43 | assert node.accept(visitor) == visitor._visit_default() 44 | -------------------------------------------------------------------------------- /tests/nodes/test_page.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.page import ContainerNode, DocumentNode, HorizontalRuleNode 2 | 3 | 4 | def test_horizontal_rule_node(): 5 | node = HorizontalRuleNode( 6 | subtype="type1", 7 | args=["value1", "value2"], 8 | tags=["tag1", "tag2"], 9 | kwargs={"key1": "text1", "key2": "text2"}, 10 | ) 11 | 12 | assert node.subtype == "type1" 13 | assert node.args == ["value1", "value2"] 14 | assert node.tags == ["tag1", "tag2"] 15 | assert node.kwargs == {"key1": "text1", "key2": "text2"} 16 | assert node.node_type == "horizontal_rule" 17 | assert node == HorizontalRuleNode( 18 | subtype="type1", 19 | args=["value1", "value2"], 20 | tags=["tag1", "tag2"], 21 | kwargs={"key1": "text1", "key2": "text2"}, 22 | ) 23 | 24 | 25 | def test_container_node(): 26 | node = ContainerNode() 27 | 28 | assert node.children == [] 29 | assert node.args == [] 30 | assert node.kwargs == {} 31 | assert node.node_type == "container" 32 | assert node == ContainerNode() 33 | 34 | 35 | def test_document_node(): 36 | node = DocumentNode() 37 | 38 | assert node.children == [] 39 | assert node.args == [] 40 | assert node.kwargs == {} 41 | assert node.node_type == "document" 42 | assert node == DocumentNode() 43 | -------------------------------------------------------------------------------- /tests/nodes/test_paragraph.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.inline import TextNode 2 | from mau.nodes.paragraph import ParagraphNode 3 | 4 | 5 | def test_paragraph_node(): 6 | node = ParagraphNode( 7 | children=[TextNode("sometext")], 8 | args=["value1", "value2"], 9 | tags=["tag1", "tag2"], 10 | kwargs={"key1": "text1", "key2": "text2"}, 11 | ) 12 | 13 | assert node.children == [TextNode("sometext")] 14 | assert node.args == ["value1", "value2"] 15 | assert node.tags == ["tag1", "tag2"] 16 | assert node.kwargs == {"key1": "text1", "key2": "text2"} 17 | assert node.node_type == "paragraph" 18 | assert node == ParagraphNode( 19 | children=[TextNode("sometext")], 20 | args=["value1", "value2"], 21 | tags=["tag1", "tag2"], 22 | kwargs={"key1": "text1", "key2": "text2"}, 23 | ) 24 | -------------------------------------------------------------------------------- /tests/nodes/test_references.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/nodes/test_references.py -------------------------------------------------------------------------------- /tests/nodes/test_source.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.inline import TextNode 2 | from mau.nodes.source import CalloutNode, CalloutsEntryNode, SourceNode 3 | 4 | 5 | def test_callouts_entry_node(): 6 | node = CalloutsEntryNode("somemarker", "somevalue") 7 | 8 | assert node.marker == "somemarker" 9 | assert node.value == "somevalue" 10 | assert node.node_type == "callouts_entry" 11 | assert node == CalloutsEntryNode("somemarker", "somevalue") 12 | 13 | 14 | def test_callout_node(): 15 | node = CalloutNode("2", "somemarker") 16 | 17 | assert node.line == "2" 18 | assert node.marker == "somemarker" 19 | assert node.node_type == "callout" 20 | assert node == CalloutNode("2", "somemarker") 21 | 22 | 23 | def test_source_node(): 24 | node = SourceNode( 25 | code=[TextNode("somecode")], 26 | callouts=[CalloutsEntryNode("somemarker", "somevalue")], 27 | markers=[CalloutNode("2", "somemarker")], 28 | ) 29 | 30 | assert node.code == [TextNode("somecode")] 31 | assert node.language == "text" 32 | assert node.callouts == [CalloutsEntryNode("somemarker", "somevalue")] 33 | assert node.delimiter == ":" 34 | assert node.markers == [CalloutNode("2", "somemarker")] 35 | assert node.highlights == [] 36 | assert node.classes == [] 37 | assert node.title is None 38 | assert node.preprocessor is None 39 | assert node.args == [] 40 | assert node.kwargs == {} 41 | assert node.tags == [] 42 | assert node.node_type == "source" 43 | assert node == SourceNode( 44 | code=[TextNode("somecode")], 45 | callouts=[CalloutsEntryNode("somemarker", "somevalue")], 46 | markers=[CalloutNode("2", "somemarker")], 47 | ) 48 | -------------------------------------------------------------------------------- /tests/nodes/test_toc.py: -------------------------------------------------------------------------------- 1 | from mau.nodes.header import HeaderNode 2 | from mau.nodes.toc import TocEntryNode, TocNode 3 | 4 | 5 | def test_toc_entry_node(): 6 | node = TocEntryNode(value="Header 1", anchor="header-1", children=[]) 7 | 8 | assert node.value == "Header 1" 9 | assert node.anchor == "header-1" 10 | assert node.children == [] 11 | assert node.node_type == "toc_entry" 12 | assert node == TocEntryNode(value="Header 1", anchor="header-1", children=[]) 13 | 14 | 15 | def test_toc_node(): 16 | node = TocNode( 17 | children=[ 18 | HeaderNode( 19 | "somevalue", 20 | "somelevel", 21 | "someanchor", 22 | args=["hvalue1", "hvalue2"], 23 | kwargs={"hkey1": "htext1", "hkey2": "htext2"}, 24 | ) 25 | ], 26 | args=["value1", "value2"], 27 | tags=["tag1", "tag2"], 28 | kwargs={"key1": "text1", "key2": "text2"}, 29 | ) 30 | 31 | assert node.children == [ 32 | HeaderNode( 33 | "somevalue", 34 | "somelevel", 35 | "someanchor", 36 | args=["hvalue1", "hvalue2"], 37 | kwargs={"hkey1": "htext1", "hkey2": "htext2"}, 38 | ) 39 | ] 40 | assert node.args == ["value1", "value2"] 41 | assert node.tags == ["tag1", "tag2"] 42 | assert node.kwargs == {"key1": "text1", "key2": "text2"} 43 | assert node.node_type == "toc" 44 | assert node == TocNode( 45 | children=[ 46 | HeaderNode( 47 | "somevalue", 48 | "somelevel", 49 | "someanchor", 50 | args=["hvalue1", "hvalue2"], 51 | kwargs={"hkey1": "htext1", "hkey2": "htext2"}, 52 | ) 53 | ], 54 | args=["value1", "value2"], 55 | tags=["tag1", "tag2"], 56 | kwargs={"key1": "text1", "key2": "text2"}, 57 | ) 58 | -------------------------------------------------------------------------------- /tests/parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/parsers/__init__.py -------------------------------------------------------------------------------- /tests/parsers/main_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/parsers/main_parser/__init__.py -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_arguments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mau.errors import MauErrorException 3 | from mau.lexers.main_lexer import MainLexer 4 | from mau.parsers.main_parser import MainParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(MainLexer, MainParser) 9 | 10 | runner = parser_runner_factory(MainLexer, MainParser) 11 | 12 | 13 | def test_arguments_empty(): 14 | source = """ 15 | [] 16 | """ 17 | 18 | assert runner(source).attributes_manager.attributes == ([], {}, [], None) 19 | 20 | 21 | def test_arguments_subtype(): 22 | source = """ 23 | [*subtype] 24 | """ 25 | 26 | assert runner(source).attributes_manager.attributes == ([], {}, [], "subtype") 27 | 28 | 29 | def test_arguments_multiple_subtypes(): 30 | source = """ 31 | [*subtype1, *subtype2] 32 | """ 33 | 34 | with pytest.raises(MauErrorException): 35 | assert runner(source).attributes_manager.attributes == ([], {}, [], "subtype1") 36 | 37 | 38 | def test_arguments_args(): 39 | source = """ 40 | [arg1,arg2] 41 | """ 42 | 43 | assert runner(source).attributes_manager.attributes == ( 44 | ["arg1", "arg2"], 45 | {}, 46 | [], 47 | None, 48 | ) 49 | 50 | 51 | def test_arguments_kwargs(): 52 | source = """ 53 | [key1=value1,key2=value2] 54 | """ 55 | 56 | assert runner(source).attributes_manager.attributes == ( 57 | [], 58 | {"key1": "value1", "key2": "value2"}, 59 | [], 60 | None, 61 | ) 62 | 63 | 64 | def test_arguments_support_variables(): 65 | source = """ 66 | :arg1:number1 67 | :value1:42 68 | 69 | [{arg1},key1={value1}] 70 | """ 71 | 72 | assert runner(source).attributes_manager.attributes == ( 73 | ["number1"], 74 | {"key1": "42"}, 75 | [], 76 | None, 77 | ) 78 | 79 | 80 | def test_arguments_tags(): 81 | source = """ 82 | [#tag1,#tag2] 83 | """ 84 | 85 | assert runner(source).attributes_manager.attributes == ( 86 | [], 87 | {}, 88 | ["tag1", "tag2"], 89 | None, 90 | ) 91 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_block_engine_raw.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.block import BlockNode 3 | from mau.nodes.inline import RawNode 4 | from mau.parsers.main_parser import MainParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(MainLexer, MainParser) 9 | 10 | runner = parser_runner_factory(MainLexer, MainParser) 11 | 12 | 13 | def test_block_raw_engine(): 14 | source = """ 15 | [*block, engine=raw] 16 | ---- 17 | Raw content 18 | on multiple lines 19 | ---- 20 | Secondary content 21 | on multiple lines as well 22 | """ 23 | 24 | assert runner(source).nodes == [ 25 | BlockNode( 26 | subtype="block", 27 | children=[ 28 | RawNode("Raw content"), 29 | RawNode("on multiple lines"), 30 | ], 31 | secondary_children=[ 32 | RawNode("Secondary content"), 33 | RawNode("on multiple lines as well"), 34 | ], 35 | classes=[], 36 | title=None, 37 | engine="raw", 38 | preprocessor="none", 39 | args=[], 40 | kwargs={}, 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_block_other.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.block import BlockNode 3 | from mau.nodes.inline import StyleNode, TextNode 4 | from mau.nodes.paragraph import ParagraphNode 5 | from mau.parsers.main_parser import MainParser 6 | 7 | from tests.helpers import init_parser_factory, parser_runner_factory 8 | 9 | init_parser = init_parser_factory(MainLexer, MainParser) 10 | 11 | runner = parser_runner_factory(MainLexer, MainParser) 12 | 13 | 14 | def test_admonition(): 15 | source = """ 16 | [*admonition,someclass,someicon] 17 | ---- 18 | Content 19 | ---- 20 | """ 21 | 22 | assert runner(source).nodes == [ 23 | BlockNode( 24 | subtype="admonition", 25 | children=[ 26 | ParagraphNode( 27 | children=[ 28 | TextNode("Content"), 29 | ] 30 | ), 31 | ], 32 | secondary_children=[], 33 | classes=[], 34 | title=None, 35 | engine=None, 36 | preprocessor="none", 37 | args=[], 38 | kwargs={"class": "someclass", "icon": "someicon"}, 39 | ) 40 | ] 41 | 42 | 43 | def test_parse_block_quote_attribution_in_secondary_content(): 44 | source = """ 45 | [*quote] 46 | ---- 47 | Learn about the Force, Luke. 48 | ---- 49 | _Star Wars_, 1977 50 | """ 51 | 52 | assert runner(source).nodes == [ 53 | BlockNode( 54 | subtype="quote", 55 | children=[ 56 | ParagraphNode( 57 | children=[ 58 | TextNode("Learn about the Force, Luke."), 59 | ] 60 | ), 61 | ], 62 | secondary_children=[ 63 | ParagraphNode( 64 | children=[ 65 | StyleNode( 66 | value="underscore", 67 | children=[ 68 | TextNode("Star Wars"), 69 | ], 70 | ), 71 | TextNode(", 1977"), 72 | ] 73 | ), 74 | ], 75 | classes=[], 76 | title=None, 77 | engine=None, 78 | preprocessor="none", 79 | args=[], 80 | kwargs={}, 81 | ) 82 | ] 83 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_block_source.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/parsers/main_parser/test_block_source.py -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_content.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.content import ContentImageNode, ContentNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.parsers.main_parser import MainParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(MainLexer, MainParser) 9 | 10 | runner = parser_runner_factory(MainLexer, MainParser) 11 | 12 | 13 | def test_include_content(): 14 | source = """ 15 | ["text", #tag1, key1=value1, key2=value2] 16 | << ctype1:/path/to/it,/another/path 17 | """ 18 | 19 | assert runner(source).nodes == [ 20 | ContentNode( 21 | uris=["/path/to/it", "/another/path"], 22 | content_type="ctype1", 23 | args=["text"], 24 | tags=["tag1"], 25 | kwargs={"key1": "value1", "key2": "value2"}, 26 | ) 27 | ] 28 | 29 | 30 | def test_include_content_with_subtype(): 31 | source = """ 32 | [*subtype1] 33 | << ctype1:/path/to/it 34 | """ 35 | 36 | assert runner(source).nodes == [ 37 | ContentNode( 38 | uris=["/path/to/it"], 39 | content_type="ctype1", 40 | subtype="subtype1", 41 | args=[], 42 | tags=[], 43 | kwargs={}, 44 | ) 45 | ] 46 | 47 | 48 | def test_include_image_with_only_path(): 49 | source = """ 50 | << image:/path/to/it.jpg 51 | """ 52 | 53 | assert runner(source).nodes == [ContentImageNode("/path/to/it.jpg")] 54 | 55 | 56 | def test_include_image_with_http(): 57 | source = """ 58 | << image:https:///some.domain/path/to/it.jpg 59 | """ 60 | 61 | assert runner(source).nodes == [ 62 | ContentImageNode("https:///some.domain/path/to/it.jpg") 63 | ] 64 | 65 | 66 | def test_include_image_with_arguments(): 67 | source = """ 68 | ["alt text", #tag1, key1=value1, key2=value2, classes="class1,class2"] 69 | << image:/path/to/it.jpg, 70 | """ 71 | 72 | assert runner(source).nodes == [ 73 | ContentImageNode( 74 | "/path/to/it.jpg", 75 | args=[], 76 | tags=["tag1"], 77 | kwargs={"key1": "value1", "key2": "value2"}, 78 | alt_text="alt text", 79 | classes=["class1", "class2"], 80 | ) 81 | ] 82 | 83 | 84 | def test_include_image_with_title(): 85 | source = """ 86 | . A nice caption 87 | << image:/path/to/it.jpg 88 | """ 89 | 90 | assert runner(source).nodes == [ 91 | ContentImageNode( 92 | "/path/to/it.jpg", 93 | title=SentenceNode( 94 | children=[ 95 | TextNode("A nice caption"), 96 | ] 97 | ), 98 | ) 99 | ] 100 | 101 | 102 | def test_include_image_with_subtype(): 103 | source = """ 104 | [*subtype1] 105 | << image:/path/to/it.jpg 106 | """ 107 | 108 | assert runner(source).nodes == [ 109 | ContentImageNode( 110 | "/path/to/it.jpg", 111 | subtype="subtype1", 112 | ), 113 | ] 114 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_horizontal_rule.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.page import HorizontalRuleNode 3 | from mau.parsers.main_parser import MainParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(MainLexer, MainParser) 8 | 9 | runner = parser_runner_factory(MainLexer, MainParser) 10 | 11 | 12 | def test_horizontal_rule(): 13 | source = "---" 14 | 15 | expected = [HorizontalRuleNode()] 16 | 17 | assert runner(source).nodes == expected 18 | 19 | 20 | def test_horizontal_rule_with_arguments(): 21 | source = """ 22 | [*break,arg1,key1=value1] 23 | --- 24 | """ 25 | 26 | expected = [ 27 | HorizontalRuleNode( 28 | subtype="break", 29 | args=["arg1"], 30 | kwargs={ 31 | "key1": "value1", 32 | }, 33 | ), 34 | ] 35 | 36 | assert runner(source).nodes == expected 37 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_paragraph.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.inline import SentenceNode, TextNode 3 | from mau.nodes.macros import MacroLinkNode 4 | from mau.nodes.paragraph import ParagraphNode 5 | from mau.parsers.main_parser import MainParser 6 | 7 | from tests.helpers import init_parser_factory, parser_runner_factory 8 | 9 | init_parser = init_parser_factory(MainLexer, MainParser) 10 | 11 | runner = parser_runner_factory(MainLexer, MainParser) 12 | 13 | 14 | def test_parse_paragraphs(): 15 | source = """ 16 | This is a paragraph. 17 | This is part of the same paragraph. 18 | 19 | This is a new paragraph. 20 | """ 21 | 22 | assert runner(source).nodes == [ 23 | ParagraphNode( 24 | children=[ 25 | TextNode("This is a paragraph. This is part of the same paragraph."), 26 | ], 27 | args=[], 28 | kwargs={}, 29 | ), 30 | ParagraphNode( 31 | children=[ 32 | TextNode("This is a new paragraph."), 33 | ], 34 | args=[], 35 | kwargs={}, 36 | ), 37 | ] 38 | 39 | 40 | def test_parse_paragraph_starting_with_a_macro(): 41 | source = "[link](http://some.where,This) is the link I want" 42 | 43 | assert runner(source).nodes == [ 44 | ParagraphNode( 45 | children=[ 46 | MacroLinkNode(target="http://some.where", children=[TextNode("This")]), 47 | TextNode(" is the link I want"), 48 | ] 49 | ) 50 | ] 51 | 52 | 53 | def test_attributes_paragraph(): 54 | source = """ 55 | [*type, arg1,key1=value1] 56 | This is text 57 | """ 58 | 59 | assert runner(source).nodes == [ 60 | ParagraphNode( 61 | children=[TextNode("This is text")], 62 | args=["arg1"], 63 | kwargs={"key1": "value1"}, 64 | subtype="type", 65 | ), 66 | ] 67 | 68 | 69 | def test_paragraph_title(): 70 | source = """ 71 | .Title 72 | This is text 73 | """ 74 | 75 | parser = runner(source) 76 | 77 | assert parser.nodes == [ 78 | ParagraphNode( 79 | children=[TextNode("This is text")], 80 | title=SentenceNode(children=[TextNode("Title")]), 81 | ), 82 | ] 83 | 84 | paragraph_node = parser.nodes[0] 85 | text_node = paragraph_node.title.children[0] 86 | 87 | assert text_node.parent == paragraph_node 88 | assert text_node.parent_position == "title" 89 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_parent.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.parsers.main_parser import MainParser 3 | 4 | from tests.helpers import parser_runner_factory 5 | 6 | mainrunner = parser_runner_factory(MainLexer, MainParser) 7 | 8 | 9 | def test_parent_block(): 10 | source = """ 11 | ---- 12 | Content 13 | ---- 14 | """ 15 | 16 | nodes = mainrunner(source).nodes 17 | 18 | block_node = nodes[0] 19 | paragraph_node = block_node.children[0] 20 | 21 | assert paragraph_node.parent is block_node 22 | 23 | 24 | def test_parent_paragraph(): 25 | source = "Some text _and style_ and *more style* here" 26 | 27 | nodes = mainrunner(source).nodes 28 | 29 | paragraph_node = nodes[0] 30 | text_node = paragraph_node.children[0] 31 | style_node = paragraph_node.children[1] 32 | 33 | assert text_node.parent is paragraph_node 34 | assert style_node.parent is paragraph_node 35 | -------------------------------------------------------------------------------- /tests/parsers/main_parser/test_title.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.main_lexer import MainLexer 2 | from mau.nodes.block import BlockNode 3 | from mau.nodes.content import ContentNode 4 | from mau.nodes.inline import SentenceNode, TextNode 5 | from mau.nodes.nodes import Node 6 | from mau.parsers.main_parser import MainParser 7 | from mau.text_buffer.context import Context 8 | 9 | from tests.helpers import init_parser_factory, parser_runner_factory 10 | 11 | init_parser = init_parser_factory(MainLexer, MainParser) 12 | 13 | runner = parser_runner_factory(MainLexer, MainParser) 14 | 15 | 16 | def test_initial_title(): 17 | source = """ 18 | """ 19 | 20 | parser = runner(source) 21 | 22 | assert parser.title == (None, None) 23 | 24 | 25 | def test_push_title(): 26 | source = """ 27 | """ 28 | 29 | parser = runner(source) 30 | 31 | parser._push_title("Just a title", Context(42, 128, "main")) 32 | 33 | assert parser.title == ( 34 | "Just a title", 35 | Context(42, 128, "main"), 36 | ) 37 | 38 | 39 | def test_pop_title(): 40 | source = """ 41 | """ 42 | 43 | node = Node() 44 | 45 | parser = runner(source) 46 | 47 | parser._push_title("Just a title", Context(42, 128, "main")) 48 | 49 | title_node = parser._pop_title(node) 50 | 51 | assert title_node == SentenceNode( 52 | children=[ 53 | TextNode("Just a title"), 54 | ] 55 | ) 56 | assert title_node.parent == node 57 | assert title_node.parent_position == "title" 58 | 59 | 60 | def test_parse_title(): 61 | source = """ 62 | .Just a title 63 | """ 64 | 65 | parser = runner(source) 66 | 67 | assert parser.title == ( 68 | "Just a title", 69 | Context(1, 0, "main"), 70 | ) 71 | 72 | 73 | def test_block_uses_title(): 74 | source = """ 75 | .Just a title 76 | ---- 77 | ---- 78 | """ 79 | 80 | parser = runner(source) 81 | 82 | assert parser.nodes == [ 83 | BlockNode( 84 | subtype=None, 85 | classes=[], 86 | title=SentenceNode( 87 | children=[ 88 | TextNode("Just a title"), 89 | ] 90 | ), 91 | engine=None, 92 | preprocessor="none", 93 | args=[], 94 | kwargs={}, 95 | ) 96 | ] 97 | 98 | assert parser.title == (None, None) 99 | 100 | 101 | def test_include_content_uses_title(): 102 | source = """ 103 | .Just a title 104 | << type:uri1 105 | """ 106 | 107 | parser = runner(source) 108 | 109 | assert parser.nodes == [ 110 | ContentNode( 111 | content_type="type", 112 | uris=["uri1"], 113 | title=SentenceNode( 114 | children=[ 115 | TextNode("Just a title"), 116 | ] 117 | ), 118 | ) 119 | ] 120 | 121 | assert parser.title == (None, None) 122 | -------------------------------------------------------------------------------- /tests/parsers/test_preprocess_variables_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mau.environment.environment import Environment 3 | from mau.errors import MauErrorException 4 | from mau.lexers.preprocess_variables_lexer import PreprocessVariablesLexer 5 | from mau.nodes.inline import TextNode 6 | from mau.parsers.preprocess_variables_parser import PreprocessVariablesParser 7 | 8 | from tests.helpers import init_parser_factory, parser_runner_factory 9 | 10 | init_parser = init_parser_factory(PreprocessVariablesLexer, PreprocessVariablesParser) 11 | runner = parser_runner_factory(PreprocessVariablesLexer, PreprocessVariablesParser) 12 | 13 | 14 | def test_plain_text_with_no_variables(): 15 | source = "This is text" 16 | 17 | assert runner(source).nodes == [TextNode("This is text")] 18 | 19 | 20 | def test_plain_text_with_variables(): 21 | source = "This is text" 22 | result = runner(source, environment=Environment({"attr": "5"})) 23 | 24 | assert result.nodes == [TextNode("This is text")] 25 | 26 | 27 | def test_replace_variable(): 28 | source = "This is number {attr}" 29 | result = runner(source, environment=Environment({"attr": "5"})) 30 | 31 | assert result.nodes == [TextNode("This is number 5")] 32 | 33 | 34 | def test_manage_unclosed_curly_braces(): 35 | source = "This is {attr" 36 | result = runner(source, environment=Environment({"attr": "5"})) 37 | 38 | assert result.nodes == [TextNode("This is {attr")] 39 | 40 | 41 | def test_replace_variable_with_namespace(): 42 | source = "This is number {app.attr}" 43 | result = runner( 44 | source, 45 | environment=Environment({"app": {"attr": "5"}}), 46 | ) 47 | 48 | assert result.nodes == [TextNode("This is number 5")] 49 | 50 | 51 | def test_replace_boolean(): 52 | result = runner("This flag is {flag}", environment=Environment({"flag": True})) 53 | 54 | assert result.nodes == [TextNode("This flag is ")] 55 | 56 | 57 | def test_escape_curly_braces(): 58 | source = r"This is \{attr\}" 59 | result = runner(source, environment=Environment({"attr": "5"})) 60 | 61 | assert result.nodes == [TextNode("This is {attr}")] 62 | 63 | 64 | def test_curly_braces_in_verbatim(): 65 | source = "This is `{attr}`" 66 | result = runner(source, environment=Environment({"attr": "5"})) 67 | 68 | assert result.nodes == [TextNode("This is `{attr}`")] 69 | 70 | 71 | def test_open_verbatim(): 72 | source = "This is `{attr}" 73 | result = runner(source, environment=Environment({"attr": "5"})) 74 | 75 | assert result.nodes == [TextNode("This is `5")] 76 | 77 | 78 | def test_escape_curly_braces_in_verbatim(): 79 | source = r"This is `\{attr\}`" 80 | result = runner(source, environment=Environment({"attr": "5"})) 81 | 82 | assert result.nodes == [TextNode(r"This is `\{attr\}`")] 83 | 84 | 85 | def test_escape_other_chars(): 86 | source = r"This \_is\_ \text" 87 | result = runner(source, environment=Environment({"attr": "5"})) 88 | 89 | assert result.nodes == [TextNode(r"This \_is\_ \text")] 90 | 91 | 92 | def test_curly_braces_in_escaped_verbatim(): 93 | source = r"This is \`{attr}\`" 94 | result = runner(source, environment=Environment({"attr": "5"})) 95 | 96 | assert result.nodes == [TextNode(r"This is \`5\`")] 97 | 98 | 99 | def test_variable_not_existing(): 100 | source = "This is number {attr}" 101 | 102 | with pytest.raises(MauErrorException): 103 | runner(source, environment={}) 104 | 105 | 106 | def test_variables_can_contain_markers(): 107 | source = "A very {bold} text. Some code: {dictdef}" 108 | result = runner( 109 | source, 110 | environment=Environment({"bold": "*bold*", "dictdef": "`adict = {'a':5}`"}), 111 | ) 112 | 113 | assert result.nodes == [ 114 | TextNode("A very *bold* text. Some code: `adict = {'a':5}`") 115 | ] 116 | 117 | 118 | def test_escape_backtick(): 119 | result = runner(r"This is `\``") 120 | 121 | assert result.nodes == [TextNode(r"This is `\``")] 122 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/parsers/text_parser/__init__.py -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/parsers/text_parser/macros/__init__.py -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_arguments.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.parsers.text_parser import TextParser 3 | 4 | from tests.helpers import init_parser_factory, parser_runner_factory 5 | 6 | init_parser = init_parser_factory(TextLexer, TextParser) 7 | 8 | runner = parser_runner_factory(TextLexer, TextParser) 9 | 10 | 11 | def test_collect_macro_arguments_single_argument(): 12 | source = "(value1)" 13 | 14 | parser = init_parser(source) 15 | 16 | assert parser._collect_macro_args() == "value1" 17 | 18 | 19 | def test_collect_macro_arguments_multiple_arguments(): 20 | source = "(value1,value2)" 21 | 22 | parser = init_parser(source) 23 | 24 | assert parser._collect_macro_args() == "value1,value2" 25 | 26 | 27 | def test_collect_macro_arguments_single_argument_with_quotes(): 28 | source = '("value1")' 29 | 30 | parser = init_parser(source) 31 | 32 | assert parser._collect_macro_args() == '"value1"' 33 | 34 | 35 | def test_collect_macro_arguments_single_argument_with_quotes_and_parenthesis(): 36 | source = '("value1()")' 37 | 38 | parser = init_parser(source) 39 | 40 | assert parser._collect_macro_args() == '"value1()"' 41 | 42 | 43 | def test_collect_macro_arguments_single_argument_with_parenthesis(): 44 | source = "(value1())" 45 | 46 | parser = init_parser(source) 47 | 48 | assert parser._collect_macro_args() == "value1(" 49 | 50 | 51 | def test_collect_macro_arguments_multiple_argument_with_quotes_and_parenthesis(): 52 | source = '("value1()",value2,value3)' 53 | 54 | parser = init_parser(source) 55 | 56 | assert parser._collect_macro_args() == '"value1()",value2,value3' 57 | 58 | 59 | def test_collect_macro_arguments_multiple_argument_with_escaped_quotes(): 60 | source = r"(\"value2,value3)" 61 | 62 | parser = init_parser(source) 63 | 64 | assert parser._collect_macro_args() == r"\"value2,value3" 65 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_class.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import StyleNode, TextNode, VerbatimNode 3 | from mau.nodes.macros import MacroClassNode 4 | from mau.parsers.text_parser import TextParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(TextLexer, TextParser) 9 | 10 | runner = parser_runner_factory(TextLexer, TextParser) 11 | 12 | 13 | def test_single_class(): 14 | source = 'Some text [class]("text with that class", classname)' 15 | 16 | expected = [ 17 | TextNode("Some text "), 18 | MacroClassNode( 19 | classes=["classname"], 20 | children=[ 21 | TextNode("text with that class"), 22 | ], 23 | ), 24 | ] 25 | 26 | assert runner(source).nodes == expected 27 | 28 | 29 | def test_multiple_classes(): 30 | source = 'Some text [class]("text with that class", classname1, classname2)' 31 | 32 | expected = [ 33 | TextNode("Some text "), 34 | MacroClassNode( 35 | classes=["classname1", "classname2"], 36 | children=[ 37 | TextNode("text with that class"), 38 | ], 39 | ), 40 | ] 41 | 42 | assert runner(source).nodes == expected 43 | 44 | 45 | def test_parse_class_with_rich_text(): 46 | source = '[class]("Some text with `verbatim words` and _styled ones_", classname)' 47 | 48 | expected = [ 49 | MacroClassNode( 50 | classes=["classname"], 51 | children=[ 52 | TextNode("Some text with "), 53 | VerbatimNode("verbatim words"), 54 | TextNode(" and "), 55 | StyleNode(value="underscore", children=[TextNode("styled ones")]), 56 | ], 57 | ), 58 | ] 59 | 60 | assert runner(source).nodes == expected 61 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_footnote.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.footnotes import FootnoteNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_macro_footnote(): 13 | source = "[footnote](notename)" 14 | 15 | footnote_node = FootnoteNode() 16 | expected = [footnote_node] 17 | 18 | parser = runner(source) 19 | assert parser.nodes == expected 20 | assert parser.footnotes == {"notename": footnote_node} 21 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_generic_macro.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.macros import MacroNode 4 | from mau.parsers.text_parser import TextParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(TextLexer, TextParser) 9 | 10 | runner = parser_runner_factory(TextLexer, TextParser) 11 | 12 | 13 | def test_macro(): 14 | source = "[macroname](value1,value2)" 15 | 16 | expected = [ 17 | MacroNode( 18 | "macroname", 19 | args=["value1", "value2"], 20 | ), 21 | ] 22 | 23 | assert runner(source).nodes == expected 24 | 25 | 26 | def test_incomplete_macro(): 27 | source = "[macroname](value1" 28 | 29 | expected = [ 30 | TextNode( 31 | "[macroname](value1", 32 | ), 33 | ] 34 | 35 | assert runner(source).nodes == expected 36 | 37 | 38 | def test_macro_arguments_with_quotes(): 39 | source = '[macroname]("value1,value2")' 40 | 41 | expected = [ 42 | MacroNode( 43 | "macroname", 44 | args=["value1,value2"], 45 | ), 46 | ] 47 | 48 | assert runner(source).nodes == expected 49 | 50 | 51 | def test_macro_named_arguments(): 52 | source = "[macroname](name,arg1=value1)" 53 | 54 | expected = [ 55 | MacroNode( 56 | "macroname", 57 | args=["name"], 58 | kwargs={"arg1": "value1"}, 59 | ), 60 | ] 61 | 62 | assert runner(source).nodes == expected 63 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_header.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.macros import MacroHeaderNode 4 | from mau.parsers.text_parser import TextParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(TextLexer, TextParser) 9 | 10 | runner = parser_runner_factory(TextLexer, TextParser) 11 | 12 | 13 | def test_macro_header(): 14 | source = '[header](id, "link text")' 15 | 16 | node = MacroHeaderNode("id", children=[TextNode("link text")]) 17 | 18 | parser = runner(source) 19 | assert parser.nodes == [node] 20 | assert parser.links == [node] 21 | 22 | 23 | def test_macro_header_without_text(): 24 | source = "[header](id)" 25 | 26 | node = MacroHeaderNode("id", children=[]) 27 | 28 | parser = runner(source) 29 | assert parser.nodes == [node] 30 | assert parser.links == [node] 31 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_image.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.macros import MacroImageNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_macro_image(): 13 | source = "[image](/the/path.jpg)" 14 | 15 | expected = [ 16 | MacroImageNode("/the/path.jpg"), 17 | ] 18 | 19 | assert runner(source).nodes == expected 20 | 21 | 22 | def test_macro_image_with_alt_text(): 23 | source = '[image](/the/path.jpg,"alt name")' 24 | 25 | expected = [ 26 | MacroImageNode("/the/path.jpg", alt_text="alt name"), 27 | ] 28 | 29 | assert runner(source).nodes == expected 30 | 31 | 32 | def test_macro_image_with_width_and_height(): 33 | source = "[image](/the/path.jpg,width=1200,height=600)" 34 | 35 | expected = [ 36 | MacroImageNode("/the/path.jpg", alt_text=None, width="1200", height="600"), 37 | ] 38 | 39 | assert runner(source).nodes == expected 40 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/macros/test_link.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import StyleNode, TextNode 3 | from mau.nodes.macros import MacroLinkNode 4 | from mau.parsers.text_parser import TextParser 5 | 6 | from tests.helpers import init_parser_factory, parser_runner_factory 7 | 8 | init_parser = init_parser_factory(TextLexer, TextParser) 9 | 10 | runner = parser_runner_factory(TextLexer, TextParser) 11 | 12 | 13 | def test_macro_link(): 14 | source = '[link](https://somedomain.org/the/path, "link text")' 15 | 16 | expected = [ 17 | MacroLinkNode( 18 | "https://somedomain.org/the/path", children=[TextNode("link text")] 19 | ), 20 | ] 21 | 22 | assert runner(source).nodes == expected 23 | 24 | 25 | def test_macro_link_without_text(): 26 | source = '[link]("https://somedomain.org/the/path")' 27 | 28 | expected = [ 29 | MacroLinkNode( 30 | "https://somedomain.org/the/path", 31 | children=[TextNode("https://somedomain.org/the/path")], 32 | ), 33 | ] 34 | 35 | assert runner(source).nodes == expected 36 | 37 | 38 | def test_macro_link_with_rich_text(): 39 | source = ( 40 | '[link]("https://somedomain.org/the/path", "Some text with _styled words_")' 41 | ) 42 | 43 | expected = [ 44 | MacroLinkNode( 45 | "https://somedomain.org/the/path", 46 | children=[ 47 | TextNode("Some text with "), 48 | StyleNode(value="underscore", children=[TextNode("styled words")]), 49 | ], 50 | ), 51 | ] 52 | 53 | assert runner(source).nodes == expected 54 | 55 | 56 | def test_macro_mailto(): 57 | source = "[mailto](info@projectmau.org)" 58 | 59 | expected = [ 60 | MacroLinkNode( 61 | "mailto:info@projectmau.org", children=[TextNode("info@projectmau.org")] 62 | ), 63 | ] 64 | 65 | assert runner(source).nodes == expected 66 | 67 | 68 | def test_macro_mailto_custom_text(): 69 | source = '[mailto](info@projectmau.org, "my email")' 70 | 71 | expected = [ 72 | MacroLinkNode("mailto:info@projectmau.org", children=[TextNode("my email")]), 73 | ] 74 | 75 | assert runner(source).nodes == expected 76 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/test_basic_text.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import TextNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_empty_text(): 13 | source = "" 14 | 15 | assert runner(source).nodes == [] 16 | 17 | 18 | def test_parse_word(): 19 | source = "Word" 20 | 21 | expected = [ 22 | TextNode("Word"), 23 | ] 24 | 25 | assert runner(source).nodes == expected 26 | 27 | 28 | def test_multiple_words(): 29 | source = "Many different words" 30 | 31 | expected = [ 32 | TextNode("Many different words"), 33 | ] 34 | 35 | assert runner(source).nodes == expected 36 | 37 | 38 | def test_parse_escape_word(): 39 | source = r"\Escaped" 40 | 41 | expected = [ 42 | TextNode("Escaped"), 43 | ] 44 | 45 | assert runner(source).nodes == expected 46 | 47 | 48 | def test_parse_escape_symbol(): 49 | source = r"\"Escaped" 50 | 51 | expected = [ 52 | TextNode('"Escaped'), 53 | ] 54 | 55 | assert runner(source).nodes == expected 56 | 57 | 58 | def test_square_brackets(): 59 | source = "This contains [ and ] and [this]" 60 | 61 | expected = [ 62 | TextNode("This contains [ and ] and [this]"), 63 | ] 64 | 65 | assert runner(source).nodes == expected 66 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/test_escaped.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import TextNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_dollar(): 13 | source = "$Many different words$" 14 | 15 | expected = [ 16 | TextNode("Many different words"), 17 | ] 18 | 19 | assert runner(source).nodes == expected 20 | 21 | 22 | def test_percent(): 23 | source = "%Many different words%" 24 | 25 | expected = [ 26 | TextNode("Many different words"), 27 | ] 28 | 29 | assert runner(source).nodes == expected 30 | 31 | 32 | def test_dollar_can_escape_percent(): 33 | source = "$Many %different% words$" 34 | 35 | expected = [ 36 | TextNode("Many %different% words"), 37 | ] 38 | 39 | assert runner(source).nodes == expected 40 | 41 | 42 | def test_dollar_can_escape_backtick(): 43 | source = "$Many `different` words$" 44 | 45 | expected = [ 46 | TextNode("Many `different` words"), 47 | ] 48 | 49 | assert runner(source).nodes == expected 50 | 51 | 52 | def test_percent_can_escape_dollar(): 53 | source = "%Many $different$ words%" 54 | 55 | expected = [ 56 | TextNode("Many $different$ words"), 57 | ] 58 | 59 | assert runner(source).nodes == expected 60 | 61 | 62 | def test_percent_can_escape_backtick(): 63 | source = "%Many `different` words%" 64 | 65 | expected = [ 66 | TextNode("Many `different` words"), 67 | ] 68 | 69 | assert runner(source).nodes == expected 70 | 71 | 72 | def test_dollar_escapes_style(): 73 | source = "$Many _different_ words$" 74 | 75 | expected = [ 76 | TextNode("Many _different_ words"), 77 | ] 78 | 79 | assert runner(source).nodes == expected 80 | 81 | 82 | def test_percent_escapes_style(): 83 | source = "%Many _different_ words%" 84 | 85 | expected = [ 86 | TextNode("Many _different_ words"), 87 | ] 88 | 89 | assert runner(source).nodes == expected 90 | 91 | 92 | def test_escaped_dollar(): 93 | source = r"$\$$" 94 | 95 | expected = [ 96 | TextNode("$"), 97 | ] 98 | 99 | assert runner(source).nodes == expected 100 | 101 | 102 | def test_escaped_percent(): 103 | source = r"%\%%" 104 | 105 | expected = [ 106 | TextNode("%"), 107 | ] 108 | 109 | assert runner(source).nodes == expected 110 | 111 | 112 | def test_open_dollar(): 113 | source = r"$Many different words" 114 | 115 | expected = [ 116 | TextNode("$Many different words"), 117 | ] 118 | 119 | assert runner(source).nodes == expected 120 | 121 | 122 | def test_open_percent(): 123 | source = r"%Many different words" 124 | 125 | expected = [ 126 | TextNode("%Many different words"), 127 | ] 128 | 129 | assert runner(source).nodes == expected 130 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/test_style.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import StyleNode, TextNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_underscore(): 13 | source = "_Some text_" 14 | 15 | expected = [ 16 | StyleNode( 17 | value="underscore", 18 | children=[ 19 | TextNode("Some text"), 20 | ], 21 | ), 22 | ] 23 | 24 | assert runner(source).nodes == expected 25 | 26 | 27 | def test_star(): 28 | source = "*Some text*" 29 | 30 | expected = [ 31 | StyleNode( 32 | value="star", 33 | children=[ 34 | TextNode("Some text"), 35 | ], 36 | ), 37 | ] 38 | 39 | assert runner(source).nodes == expected 40 | 41 | 42 | def test_caret(): 43 | source = "^Some text^" 44 | 45 | expected = [ 46 | StyleNode( 47 | value="caret", 48 | children=[ 49 | TextNode("Some text"), 50 | ], 51 | ), 52 | ] 53 | 54 | assert runner(source).nodes == expected 55 | 56 | 57 | def test_tilde(): 58 | source = "~Some text~" 59 | 60 | expected = [ 61 | StyleNode( 62 | value="tilde", 63 | children=[ 64 | TextNode("Some text"), 65 | ], 66 | ), 67 | ] 68 | 69 | assert runner(source).nodes == expected 70 | 71 | 72 | def test_style_within_style(): 73 | source = "_*Words with two styles*_" 74 | 75 | expected = [ 76 | StyleNode( 77 | value="underscore", 78 | children=[ 79 | StyleNode( 80 | value="star", 81 | children=[TextNode("Words with two styles")], 82 | ) 83 | ], 84 | ) 85 | ] 86 | 87 | assert runner(source).nodes == expected 88 | 89 | 90 | def test_double_style_cancels_itself(): 91 | source = "__Text__" 92 | 93 | expected = [ 94 | StyleNode(value="underscore"), 95 | TextNode("Text"), 96 | StyleNode(value="underscore"), 97 | ] 98 | 99 | assert runner(source).nodes == expected 100 | 101 | 102 | def test_mix_text_and_styles(): 103 | source = "Some text _and style_ and *more style* here" 104 | 105 | expected = [ 106 | TextNode("Some text "), 107 | StyleNode( 108 | value="underscore", 109 | children=[ 110 | TextNode("and style"), 111 | ], 112 | ), 113 | TextNode(" and "), 114 | StyleNode( 115 | value="star", 116 | children=[ 117 | TextNode("more style"), 118 | ], 119 | ), 120 | TextNode(" here"), 121 | ] 122 | 123 | assert runner(source).nodes == expected 124 | 125 | 126 | def test_unclosed_style(): 127 | source = "_Text" 128 | 129 | expected = [ 130 | TextNode("_Text"), 131 | ] 132 | 133 | assert runner(source).nodes == expected 134 | -------------------------------------------------------------------------------- /tests/parsers/text_parser/test_verbatim.py: -------------------------------------------------------------------------------- 1 | from mau.lexers.text_lexer import TextLexer 2 | from mau.nodes.inline import TextNode, VerbatimNode 3 | from mau.parsers.text_parser import TextParser 4 | 5 | from tests.helpers import init_parser_factory, parser_runner_factory 6 | 7 | init_parser = init_parser_factory(TextLexer, TextParser) 8 | 9 | runner = parser_runner_factory(TextLexer, TextParser) 10 | 11 | 12 | def test_verbatim(): 13 | source = "`Many different words`" 14 | 15 | expected = [ 16 | VerbatimNode("Many different words"), 17 | ] 18 | 19 | assert runner(source).nodes == expected 20 | 21 | 22 | def test_verbatim_escaped_backtick(): 23 | source = r"`\``" 24 | 25 | expected = [ 26 | VerbatimNode("`"), 27 | ] 28 | 29 | assert runner(source).nodes == expected 30 | 31 | 32 | def test_verbatim_style_inside_verbatim(): 33 | source = r"`_Many different words_`" 34 | 35 | expected = [ 36 | VerbatimNode("_Many different words_"), 37 | ] 38 | 39 | assert runner(source).nodes == expected 40 | 41 | 42 | def test_verbatim_open(): 43 | source = r"`Many different words" 44 | 45 | expected = [ 46 | TextNode("`Many different words"), 47 | ] 48 | 49 | assert runner(source).nodes == expected 50 | 51 | 52 | def test_verbatim_escape_characters(): 53 | source = r"`$Many$ %different% \words`" 54 | 55 | expected = [ 56 | VerbatimNode(r"$Many$ %different% \words"), 57 | ] 58 | 59 | assert runner(source).nodes == expected 60 | -------------------------------------------------------------------------------- /tests/text_buffer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/text_buffer/__init__.py -------------------------------------------------------------------------------- /tests/text_buffer/test_context.py: -------------------------------------------------------------------------------- 1 | from mau.text_buffer.text_buffer import Context 2 | 3 | 4 | def test_text_buffer_current_line(): 5 | ctx = Context(line=1, column=7, source="main") 6 | 7 | assert ctx.asdict() == { 8 | "line": 1, 9 | "column": 7, 10 | "source": "main", 11 | } 12 | assert ctx == Context(line=1, column=7, source="main") 13 | assert ctx != (1, 7) 14 | -------------------------------------------------------------------------------- /tests/tokens/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/tokens/__init__.py -------------------------------------------------------------------------------- /tests/tokens/test_tokens.py: -------------------------------------------------------------------------------- 1 | from mau.tokens.tokens import Context, Token 2 | 3 | 4 | def test_token_accepts_type_and_value(): 5 | token = Token("sometype", "somevalue") 6 | 7 | assert token.type == "sometype" 8 | assert token.value == "somevalue" 9 | assert token != "somevalue" 10 | 11 | 12 | def test_token_keeps_value_none(): 13 | token = Token("sometype", None) 14 | 15 | assert token.type == "sometype" 16 | assert token.value == "" 17 | 18 | 19 | def test_token_value_defaults_to_none(): 20 | token = Token("sometype") 21 | 22 | assert token.type == "sometype" 23 | assert token.value == "" 24 | 25 | 26 | def test_token_equality(): 27 | assert Token("sometype", "somevalue") == Token("sometype", "somevalue") 28 | 29 | 30 | def test_token_length(): 31 | token = Token("sometype", "somevalue") 32 | 33 | assert len(token) == len("somevalue") 34 | assert bool(token) is True 35 | 36 | 37 | def test_empty_token_has_length_zero(): 38 | token = Token("sometype") 39 | 40 | assert len(token) == 0 41 | assert bool(token) is True 42 | 43 | 44 | def test_token_accepts_context(): 45 | context = Context(source="main", line=123, column=456) 46 | 47 | token = Token("sometype", "somevalue", context=context) 48 | 49 | assert token.type == "sometype" 50 | assert token.value == "somevalue" 51 | assert token.context == context 52 | 53 | 54 | def test_token_equality_ignores_context(): 55 | context = Context(source="main", line=123, column=456) 56 | 57 | assert Token("sometype", "somevalue", context=context) == Token( 58 | "sometype", "somevalue" 59 | ) 60 | assert Token("sometype", "somevalue") == Token( 61 | "sometype", "somevalue", context=context 62 | ) 63 | 64 | 65 | def test_token_equality_accepts_none(): 66 | assert Token("sometype", "somevalue") is not None 67 | 68 | 69 | def test_token_equality_with_any(): 70 | assert Token("sometype", "somevalue") == Token("sometype") 71 | 72 | 73 | def test_token_match_considers_context(): 74 | context = Context(source="main", line=123, column=456) 75 | 76 | assert not Token("sometype", "somevalue", context=context).match( 77 | Token("sometype", "somevalue") 78 | ) 79 | assert not Token("sometype", "somevalue").match( 80 | Token("sometype", "somevalue", context=context) 81 | ) 82 | assert Token("sometype", "somevalue", context=context).match( 83 | Token("sometype", "somevalue", context=context) 84 | ) 85 | -------------------------------------------------------------------------------- /tests/visitors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/visitors/__init__.py -------------------------------------------------------------------------------- /tests/visitors/base_visitor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-Mau/mau/baf8e41d1c90039c05224beaf452ef6040d57a20/tests/visitors/base_visitor/__init__.py -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_content.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.content import ContentImageNode, ContentNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.visitors.base_visitor import BaseVisitor 5 | 6 | 7 | def test_content_node(): 8 | visitor = BaseVisitor(Environment()) 9 | 10 | node = ContentNode( 11 | content_type="sometype", 12 | uris=["/uri1", "/uri2"], 13 | title=SentenceNode(children=[TextNode("sometitle")]), 14 | args=["arg1", "arg2"], 15 | kwargs={"key1": "value1"}, 16 | tags=["tag1", "tag2"], 17 | ) 18 | 19 | result = visitor.visit(node) 20 | 21 | assert result == { 22 | "data": { 23 | "type": "content", 24 | "content_type": "sometype", 25 | "uris": ["/uri1", "/uri2"], 26 | "title": { 27 | "data": { 28 | "content": [ 29 | { 30 | "data": { 31 | "type": "text", 32 | "value": "sometitle", 33 | "args": [], 34 | "kwargs": {}, 35 | "subtype": None, 36 | "tags": [], 37 | } 38 | }, 39 | ], 40 | "type": "sentence", 41 | "args": [], 42 | "kwargs": {}, 43 | "subtype": None, 44 | "tags": [], 45 | }, 46 | }, 47 | "args": ["arg1", "arg2"], 48 | "kwargs": {"key1": "value1"}, 49 | "tags": ["tag1", "tag2"], 50 | "subtype": None, 51 | } 52 | } 53 | 54 | 55 | def test_content_image_node(): 56 | visitor = BaseVisitor(Environment()) 57 | 58 | node = ContentImageNode( 59 | uri="someuri", 60 | alt_text="sometext", 61 | classes=["class1", "class2"], 62 | title=SentenceNode(children=[TextNode("sometitle")]), 63 | args=["arg1", "arg2"], 64 | kwargs={"key1": "value1"}, 65 | tags=["tag1", "tag2"], 66 | ) 67 | 68 | result = visitor.visit(node) 69 | 70 | assert result == { 71 | "data": { 72 | "type": "content_image", 73 | "uri": "someuri", 74 | "title": { 75 | "data": { 76 | "content": [ 77 | { 78 | "data": { 79 | "type": "text", 80 | "value": "sometitle", 81 | "args": [], 82 | "kwargs": {}, 83 | "subtype": None, 84 | "tags": [], 85 | } 86 | }, 87 | ], 88 | "type": "sentence", 89 | "args": [], 90 | "kwargs": {}, 91 | "subtype": None, 92 | "tags": [], 93 | }, 94 | }, 95 | "alt_text": "sometext", 96 | "classes": ["class1", "class2"], 97 | "args": ["arg1", "arg2"], 98 | "kwargs": {"key1": "value1"}, 99 | "tags": ["tag1", "tag2"], 100 | "subtype": None, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_footnotes.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.footnotes import FootnoteNode, FootnotesNode 3 | from mau.nodes.inline import TextNode 4 | from mau.visitors.base_visitor import BaseVisitor 5 | 6 | 7 | def test_footnotes(): 8 | visitor = BaseVisitor(Environment()) 9 | 10 | node = FootnotesNode( 11 | children=[ 12 | FootnoteNode( 13 | children=[TextNode("Footnote 1")], 14 | number="1", 15 | reference_anchor="anchor-1", 16 | content_anchor="anchor-1-def", 17 | ).to_entry(), 18 | FootnoteNode( 19 | children=[TextNode("Footnote 2")], 20 | number="2", 21 | reference_anchor="anchor-2", 22 | content_anchor="anchor-2-def", 23 | ).to_entry(), 24 | ], 25 | args=["arg1", "arg2"], 26 | kwargs={"key1": "value1"}, 27 | tags=["tag1", "tag2"], 28 | ) 29 | 30 | result = visitor.visit(node) 31 | 32 | assert result == { 33 | "data": { 34 | "type": "footnotes", 35 | "entries": [ 36 | { 37 | "data": { 38 | "content": [ 39 | { 40 | "data": { 41 | "type": "text", 42 | "value": "Footnote 1", 43 | "args": [], 44 | "kwargs": {}, 45 | "subtype": None, 46 | "tags": [], 47 | } 48 | }, 49 | ], 50 | "content_anchor": "anchor-1-def", 51 | "number": "1", 52 | "reference_anchor": "anchor-1", 53 | "type": "footnotes_entry", 54 | "args": [], 55 | "kwargs": {}, 56 | "subtype": None, 57 | "tags": [], 58 | } 59 | }, 60 | { 61 | "data": { 62 | "content": [ 63 | { 64 | "data": { 65 | "type": "text", 66 | "value": "Footnote 2", 67 | "args": [], 68 | "kwargs": {}, 69 | "subtype": None, 70 | "tags": [], 71 | } 72 | }, 73 | ], 74 | "content_anchor": "anchor-2-def", 75 | "number": "2", 76 | "reference_anchor": "anchor-2", 77 | "type": "footnotes_entry", 78 | "args": [], 79 | "kwargs": {}, 80 | "subtype": None, 81 | "tags": [], 82 | } 83 | }, 84 | ], 85 | "args": ["arg1", "arg2"], 86 | "kwargs": {"key1": "value1"}, 87 | "tags": ["tag1", "tag2"], 88 | "subtype": None, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_header.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.header import HeaderNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.visitors.base_visitor import BaseVisitor 5 | 6 | 7 | def test_header_node(): 8 | visitor = BaseVisitor(Environment()) 9 | 10 | node = HeaderNode( 11 | value=SentenceNode(children=[TextNode("Just some text")]), 12 | level="3", 13 | anchor="someanchor", 14 | args=["arg1", "arg2"], 15 | kwargs={"key1": "value1"}, 16 | tags=["tag1", "tag2"], 17 | ) 18 | 19 | result = visitor.visit(node) 20 | 21 | assert result == { 22 | "data": { 23 | "type": "header", 24 | "value": { 25 | "data": { 26 | "type": "sentence", 27 | "content": [ 28 | { 29 | "data": { 30 | "type": "text", 31 | "value": "Just some text", 32 | "subtype": None, 33 | "args": [], 34 | "kwargs": {}, 35 | "tags": [], 36 | }, 37 | } 38 | ], 39 | "subtype": None, 40 | "args": [], 41 | "kwargs": {}, 42 | "tags": [], 43 | } 44 | }, 45 | "level": 3, 46 | "anchor": "someanchor", 47 | "subtype": None, 48 | "args": ["arg1", "arg2"], 49 | "kwargs": {"key1": "value1"}, 50 | "tags": ["tag1", "tag2"], 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_lists.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.lists import ListItemNode, ListNode 4 | from mau.visitors.base_visitor import BaseVisitor 5 | 6 | 7 | def test_list_item_node(): 8 | visitor = BaseVisitor(Environment()) 9 | 10 | node = ListItemNode(level="4", children=[TextNode("Just some text.")]) 11 | 12 | result = visitor.visit(node) 13 | 14 | assert result == { 15 | "data": { 16 | "type": "list_item", 17 | "level": 4, 18 | "content": [ 19 | { 20 | "data": { 21 | "type": "text", 22 | "value": "Just some text.", 23 | "args": [], 24 | "kwargs": {}, 25 | "subtype": None, 26 | "tags": [], 27 | } 28 | } 29 | ], 30 | "args": [], 31 | "kwargs": {}, 32 | "subtype": None, 33 | "tags": [], 34 | } 35 | } 36 | 37 | 38 | def test_list_node(): 39 | visitor = BaseVisitor(Environment()) 40 | 41 | node = ListNode( 42 | ordered=True, 43 | main_node=True, 44 | start=42, 45 | children=[ListItemNode(level="4", children=[TextNode("Just some text.")])], 46 | args=["arg1", "arg2"], 47 | kwargs={"key1": "value1", "start": 4}, 48 | tags=["tag1", "tag2"], 49 | ) 50 | 51 | result = visitor.visit(node) 52 | 53 | assert result == { 54 | "data": { 55 | "type": "list", 56 | "start": 42, 57 | "items": [ 58 | { 59 | "data": { 60 | "type": "list_item", 61 | "level": 4, 62 | "content": [ 63 | { 64 | "data": { 65 | "type": "text", 66 | "value": "Just some text.", 67 | "args": [], 68 | "kwargs": {}, 69 | "subtype": None, 70 | "tags": [], 71 | } 72 | } 73 | ], 74 | "args": [], 75 | "kwargs": {}, 76 | "subtype": None, 77 | "tags": [], 78 | } 79 | } 80 | ], 81 | "main_node": True, 82 | "ordered": True, 83 | "args": ["arg1", "arg2"], 84 | "kwargs": {"key1": "value1", "start": 4}, 85 | "tags": ["tag1", "tag2"], 86 | "subtype": None, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_page.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.page import DocumentNode, HorizontalRuleNode 4 | from mau.nodes.paragraph import ParagraphNode 5 | from mau.visitors.base_visitor import BaseVisitor 6 | 7 | 8 | def test_horizontal_rule_node(): 9 | visitor = BaseVisitor(Environment()) 10 | 11 | node = HorizontalRuleNode( 12 | args=["arg1", "arg2"], 13 | kwargs={"key1": "value1"}, 14 | tags=["tag1", "tag2"], 15 | ) 16 | 17 | result = visitor.visit(node) 18 | 19 | assert result == { 20 | "data": { 21 | "type": "horizontal_rule", 22 | "args": ["arg1", "arg2"], 23 | "kwargs": {"key1": "value1"}, 24 | "tags": ["tag1", "tag2"], 25 | "subtype": None, 26 | } 27 | } 28 | 29 | 30 | def test_document_node(): 31 | visitor = BaseVisitor(Environment()) 32 | 33 | node = DocumentNode( 34 | children=[ 35 | ParagraphNode( 36 | children=[TextNode("Just some text")], 37 | args=["arg1", "arg2"], 38 | kwargs={"key1": "value1"}, 39 | tags=["tag1", "tag2"], 40 | ) 41 | ], 42 | args=["arg3", "arg4"], 43 | kwargs={"key2": "value2"}, 44 | tags=["tag3", "tag4"], 45 | ) 46 | 47 | result = visitor.visit(node) 48 | 49 | assert result == { 50 | "data": { 51 | "type": "document", 52 | "content": [ 53 | { 54 | "data": { 55 | "type": "paragraph", 56 | "content": [ 57 | { 58 | "data": { 59 | "type": "text", 60 | "value": "Just some text", 61 | "args": [], 62 | "kwargs": {}, 63 | "subtype": None, 64 | "tags": [], 65 | } 66 | } 67 | ], 68 | "title": {}, 69 | "subtype": None, 70 | "args": ["arg1", "arg2"], 71 | "kwargs": {"key1": "value1"}, 72 | "tags": ["tag1", "tag2"], 73 | } 74 | } 75 | ], 76 | "args": ["arg3", "arg4"], 77 | "kwargs": {"key2": "value2"}, 78 | "tags": ["tag3", "tag4"], 79 | "subtype": None, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/visitors/base_visitor/test_paragraph.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.inline import SentenceNode, TextNode 3 | from mau.nodes.paragraph import ParagraphNode 4 | from mau.visitors.base_visitor import BaseVisitor 5 | 6 | 7 | def test_paragraph_node(): 8 | visitor = BaseVisitor(Environment()) 9 | 10 | node = ParagraphNode( 11 | title=SentenceNode(children=[TextNode("sometitle")]), 12 | children=[TextNode("Just some text")], 13 | args=["arg1", "arg2"], 14 | kwargs={"key1": "value1"}, 15 | tags=["tag1", "tag2"], 16 | ) 17 | 18 | result = visitor.visit(node) 19 | 20 | assert result == { 21 | "data": { 22 | "type": "paragraph", 23 | "title": { 24 | "data": { 25 | "content": [ 26 | { 27 | "data": { 28 | "type": "text", 29 | "value": "sometitle", 30 | "args": [], 31 | "kwargs": {}, 32 | "subtype": None, 33 | "tags": [], 34 | } 35 | }, 36 | ], 37 | "type": "sentence", 38 | "args": [], 39 | "kwargs": {}, 40 | "subtype": None, 41 | "tags": [], 42 | }, 43 | }, 44 | "content": [ 45 | { 46 | "data": { 47 | "type": "text", 48 | "value": "Just some text", 49 | "args": [], 50 | "kwargs": {}, 51 | "subtype": None, 52 | "tags": [], 53 | } 54 | }, 55 | ], 56 | "subtype": None, 57 | "args": ["arg1", "arg2"], 58 | "kwargs": {"key1": "value1"}, 59 | "tags": ["tag1", "tag2"], 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_content.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.content import ContentImageNode, ContentNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.visitors.jinja_visitor import JinjaVisitor 5 | 6 | 7 | def test_page_content_node(): 8 | templates = { 9 | "text.j2": "{{ value }}", 10 | "sentence.j2": "{{ content }}", 11 | "content.sometype.j2": ( 12 | "{{ title }} - " 13 | "{{ uris | join(',') }} - " 14 | "{{ args | join(',') }} - " 15 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 16 | "{{ tags | join(',') }}" 17 | ), 18 | } 19 | 20 | environment = Environment() 21 | environment.update(templates, "mau.visitor.custom_templates") 22 | visitor = JinjaVisitor(environment) 23 | 24 | node = ContentNode( 25 | "sometype", 26 | uris=["/uri1", "/uri2"], 27 | title=SentenceNode(children=[TextNode("sometitle")]), 28 | args=["arg1", "arg2"], 29 | kwargs={"key1": "value1"}, 30 | tags=["tag1", "tag2"], 31 | ) 32 | 33 | result = visitor.visit(node) 34 | 35 | assert result == "sometitle - /uri1,/uri2 - arg1,arg2 - key1:value1 - tag1,tag2" 36 | 37 | 38 | def test_content_image_node(): 39 | templates = { 40 | "text.j2": "{{ value }}", 41 | "sentence.j2": "{{ content }}", 42 | "content_image.j2": ( 43 | "{{ uri }} - {{ alt_text }} - {{ classes | join(',') }} - " 44 | "{{ title }} - {{ args | join(',') }} - " 45 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 46 | "{{ tags | join(',') }}" 47 | ), 48 | } 49 | 50 | environment = Environment() 51 | environment.update(templates, "mau.visitor.custom_templates") 52 | visitor = JinjaVisitor(environment) 53 | 54 | node = ContentImageNode( 55 | "someuri", 56 | "alttext", 57 | ["class1", "class2"], 58 | title=SentenceNode(children=[TextNode("sometitle")]), 59 | args=["arg1", "arg2"], 60 | kwargs={"key1": "value1"}, 61 | tags=["tag1", "tag2"], 62 | ) 63 | 64 | result = visitor.visit(node) 65 | 66 | assert ( 67 | result 68 | == "someuri - alttext - class1,class2 - sometitle - arg1,arg2 - key1:value1 - tag1,tag2" 69 | ) 70 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_footnotes.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.footnotes import FootnoteNode, FootnotesNode 3 | from mau.nodes.inline import TextNode 4 | from mau.visitors.jinja_visitor import JinjaVisitor 5 | 6 | 7 | def test_page_footnotes_node(): 8 | templates = { 9 | "text.j2": "{{ value }}", 10 | "footnotes.j2": "{{ entries }}", 11 | "footnotes_entry.j2": ( 12 | "{{ content }}:{{ number }}:" "{{ content_anchor }}:{{ reference_anchor }}" 13 | ), 14 | } 15 | 16 | environment = Environment() 17 | environment.update(templates, "mau.visitor.custom_templates") 18 | visitor = JinjaVisitor(environment) 19 | 20 | args = ["arg1", "arg2"] 21 | kwargs = {"key1": "value1"} 22 | tags = ["tag1", "tag2"] 23 | node = FootnotesNode( 24 | children=[ 25 | FootnoteNode( 26 | children=[TextNode("Footnote 1")], 27 | number="1", 28 | reference_anchor="anchor-1", 29 | content_anchor="anchor-1-def", 30 | ).to_entry(), 31 | FootnoteNode( 32 | children=[TextNode("Footnote 2")], 33 | number="2", 34 | reference_anchor="anchor-2", 35 | content_anchor="anchor-2-def", 36 | ).to_entry(), 37 | ], 38 | args=args, 39 | kwargs=kwargs, 40 | tags=tags, 41 | ) 42 | 43 | result = visitor.visit(node) 44 | 45 | assert ( 46 | result == "Footnote 1:1:anchor-1-def:anchor-1Footnote 2:2:anchor-2-def:anchor-2" 47 | ) 48 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_header.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.header import HeaderNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.visitors.jinja_visitor import JinjaVisitor 5 | 6 | 7 | def test_page_header_node(): 8 | templates = { 9 | "text.j2": "{{ value }}", 10 | "sentence.j2": "{{ content }}", 11 | "header.j2": ( 12 | "{{ value }} - {{ level }} - {{ anchor }} - {{ args | join(',') }} - " 13 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 14 | "{{ tags | join(',') }}" 15 | ), 16 | } 17 | 18 | environment = Environment() 19 | environment.update(templates, "mau.visitor.custom_templates") 20 | visitor = JinjaVisitor(environment) 21 | 22 | args = ["arg1", "arg2"] 23 | kwargs = {"key1": "value1"} 24 | tags = ["tag1", "tag2"] 25 | node = HeaderNode( 26 | value=SentenceNode(children=[TextNode("Just some text")]), 27 | level="3", 28 | anchor="someanchor", 29 | args=args, 30 | kwargs=kwargs, 31 | tags=tags, 32 | ) 33 | 34 | result = visitor.visit(node) 35 | 36 | assert ( 37 | result 38 | == "Just some text - 3 - someanchor - arg1,arg2 - key1:value1 - tag1,tag2" 39 | ) 40 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_lists.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.lists import ListItemNode, ListNode 4 | from mau.visitors.jinja_visitor import JinjaVisitor 5 | 6 | 7 | def test_inline_list_item_node(): 8 | templates = { 9 | "text.j2": "{{ value }}", 10 | "list_item.j2": "{{ level }} - {{ content }}", 11 | } 12 | 13 | environment = Environment() 14 | environment.update(templates, "mau.visitor.custom_templates") 15 | visitor = JinjaVisitor(environment) 16 | 17 | node = ListItemNode(level="4", children=[TextNode("Just some text.")]) 18 | 19 | result = visitor.visit(node) 20 | 21 | assert result == "4 - Just some text." 22 | 23 | 24 | def test_page_list_node(): 25 | templates = { 26 | "text.j2": "{{ value }}", 27 | "list_item.j2": "{{ level }}:{{ content }}", 28 | "list.j2": ( 29 | "{{ ordered }} - {{ items }} - {{ main_node }} - {{ args | join(',') }} - " 30 | "{{ kwargs|items|map('join', '=')|join(',') }} - {{ tags | join(',') }} - " 31 | "{{ kwargs.start }}" 32 | ), 33 | } 34 | 35 | environment = Environment() 36 | environment.update(templates, "mau.visitor.custom_templates") 37 | visitor = JinjaVisitor(environment) 38 | 39 | args = ["arg1", "arg2"] 40 | kwargs = {"key1": "value1", "start": 4} 41 | tags = ["tag1", "tag2"] 42 | node = ListNode( 43 | ordered=True, 44 | main_node=True, 45 | children=[ListItemNode(level="4", children=[TextNode("Just some text.")])], 46 | args=args, 47 | kwargs=kwargs, 48 | tags=tags, 49 | ) 50 | 51 | result = visitor.visit(node) 52 | 53 | assert ( 54 | result 55 | == "True - 4:Just some text. - True - arg1,arg2 - key1=value1,start=4 - tag1,tag2 - 4" 56 | ) 57 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_page.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.page import HorizontalRuleNode 3 | from mau.visitors.jinja_visitor import JinjaVisitor 4 | 5 | 6 | def test_page_horizontal_rule_node(): 7 | templates = { 8 | "text.j2": "{{ value }}", 9 | "horizontal_rule.j2": ( 10 | "--- - {{ args | join(',') }} - " 11 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 12 | "{{ tags | join(',') }}" 13 | ), 14 | } 15 | 16 | environment = Environment() 17 | environment.update(templates, "mau.visitor.custom_templates") 18 | 19 | visitor = JinjaVisitor(environment) 20 | 21 | args = ["arg1", "arg2"] 22 | kwargs = {"key1": "value1"} 23 | tags = ["tag1", "tag2"] 24 | node = HorizontalRuleNode(args=args, kwargs=kwargs, tags=tags) 25 | 26 | result = visitor.visit(node) 27 | 28 | assert result == "--- - arg1,arg2 - key1:value1 - tag1,tag2" 29 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_paragraph.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.block import BlockNode 3 | from mau.nodes.inline import SentenceNode, TextNode 4 | from mau.nodes.paragraph import ParagraphNode 5 | from mau.visitors.jinja_visitor import JinjaVisitor 6 | 7 | 8 | def test_page_paragraph_node(): 9 | templates = { 10 | "text.j2": "{{ value }}", 11 | "sentence.j2": "{{ content }}", 12 | "paragraph.j2": ( 13 | "{{ content }} - {{ title }} - {{ args | join(',') }} - " 14 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 15 | "{{ tags | join(',') }}" 16 | ), 17 | } 18 | 19 | environment = Environment() 20 | environment.update(templates, "mau.visitor.custom_templates") 21 | visitor = JinjaVisitor(environment) 22 | 23 | args = ["arg1", "arg2"] 24 | kwargs = {"key1": "value1"} 25 | tags = ["tag1", "tag2"] 26 | node = ParagraphNode( 27 | title=SentenceNode(children=[TextNode("sometitle")]), 28 | children=[TextNode("Just some text")], 29 | args=args, 30 | kwargs=kwargs, 31 | tags=tags, 32 | ) 33 | 34 | result = visitor.visit(node) 35 | 36 | assert result == "Just some text - sometitle - arg1,arg2 - key1:value1 - tag1,tag2" 37 | 38 | 39 | def test_page_paragraph_node_inside_block(): 40 | templates = { 41 | "block.j2": "{{ content }}", 42 | "text.j2": "{{ value }}", 43 | "paragraph.j2": ( 44 | "{{ content }} - {{ args | join(',') }} - " 45 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 46 | "{{ tags | join(',') }}" 47 | ), 48 | } 49 | 50 | environment = Environment() 51 | environment.update(templates, "mau.visitor.custom_templates") 52 | visitor = JinjaVisitor(environment) 53 | 54 | node = BlockNode( 55 | subtype="section", 56 | children=[ 57 | ParagraphNode( 58 | children=[TextNode("Just some text")], 59 | args=["arg1", "arg2"], 60 | kwargs={"key1": "value1"}, 61 | tags=["tag1", "tag2"], 62 | ), 63 | ], 64 | ) 65 | 66 | result = visitor.visit(node) 67 | 68 | assert result == "Just some text - arg1,arg2 - key1:value1 - tag1,tag2" 69 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/test_source.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.inline import TextNode 3 | from mau.nodes.source import CalloutNode, CalloutsEntryNode, SourceNode 4 | from mau.visitors.jinja_visitor import JinjaVisitor 5 | 6 | 7 | def test_source_node(): 8 | templates = { 9 | "text.j2": "{{ value }}", 10 | "callout.j2": "", 11 | "callouts_entry.j2": "{{ marker }} - {{ value }}", 12 | "source.custom.j2": "The custom template", 13 | "source.j2": "The default template", 14 | } 15 | 16 | environment = Environment() 17 | environment.update(templates, "mau.visitor.custom_templates") 18 | visitor = JinjaVisitor(environment) 19 | 20 | node = SourceNode( 21 | language="somelang", 22 | code=[ 23 | TextNode("import sys"), 24 | TextNode("import: os"), 25 | TextNode(""), 26 | TextNode('print(os.environ["HOME"])'), 27 | ], 28 | markers=[CalloutNode(1, "imp"), CalloutNode(3, "env")], 29 | callouts=[ 30 | CalloutsEntryNode("imp", "This is an import"), 31 | CalloutsEntryNode("env", "Environment variables are paramount"), 32 | ], 33 | subtype="custom", 34 | args=["arg1", "arg2"], 35 | kwargs={"key1": "value1"}, 36 | tags=["tag1", "tag2"], 37 | ) 38 | 39 | result = visitor.visit(node) 40 | 41 | assert result == "The custom template" 42 | -------------------------------------------------------------------------------- /tests/visitors/jinja_visitor/toc.py: -------------------------------------------------------------------------------- 1 | from mau.environment.environment import Environment 2 | from mau.nodes.toc import TocEntryNode, TocNode 3 | from mau.visitors.jinja_visitor import JinjaVisitor 4 | 5 | 6 | def test_page_toc_node(): 7 | templates = { 8 | "text.j2": "{{ value }}", 9 | "toc.j2": ( 10 | "{{ entries }} - {{ args | join(',') }} - " 11 | "{% for key, value in kwargs|items %}{{ key }}:{{ value }}{% endfor %} - " 12 | "{{ tags | join(',') }}" 13 | ), 14 | "toc_entry.j2": ( 15 | "{{ anchor }}:{{ value }}{% if children %} - " "{{ children }}{% endif %}" 16 | ), 17 | } 18 | 19 | environment = Environment() 20 | environment.update(templates, "mau.visitor.custom_templates") 21 | visitor = JinjaVisitor(environment) 22 | 23 | args = ["arg1", "arg2"] 24 | kwargs = {"key1": "value1"} 25 | tags = ["tag1", "tag2"] 26 | node = TocNode( 27 | entries=[ 28 | TocEntryNode( 29 | "Header 1", 30 | "header-1", 31 | children=[ 32 | TocEntryNode("Header 1.1", "header-1-1"), 33 | ], 34 | ), 35 | TocEntryNode("Header 2", "header-2"), 36 | ], 37 | args=args, 38 | kwargs=kwargs, 39 | tags=tags, 40 | ) 41 | 42 | result = visitor.visit(node) 43 | 44 | assert result == ( 45 | "header-1:Header 1 - header-1-1:Header 1.1header-2:Header 2 - " 46 | "arg1,arg2 - key1:value1 - tag1,tag2" 47 | ) 48 | --------------------------------------------------------------------------------