<img align="right" src="https://raw.githubusercontent.com/mcbeet/lectern/main/logo.png?sanitize=true" alt="logo" width="76">
# Lectern
[![GitHub Actions](https://github.com/mcbeet/lectern/workflows/CI/badge.svg)](https://github.com/mcbeet/lectern/actions)
[![PyPI](https://img.shields.io/pypi/v/lectern.svg)](https://pypi.org/project/lectern/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lectern.svg)](https://pypi.org/project/lectern/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![Discord](https://img.shields.io/discord/900530660677156924?color=7289DA&label=discord&logo=discord&logoColor=fff)](https://discord.gg/98MdSGMm8j)
> Literate Minecraft data packs and resource packs.
`@function tutorial:greeting`
```mcfunction
say Hello, world!
```
## Introduction
This markdown file is interspersed with code fragments describing the content of a Minecraft data pack. Using `lectern`, you can turn this single file into an actual data pack that can be loaded into the game.
**Features**
- Turn markdown files into data packs and resource packs
- Merge resources from several markdown files
- Convert data packs and resource packs into markdown snapshots
- Can be used as a [`beet`](https://github.com/mcbeet/beet) plugin
- Highly extensible with custom directives
- Automatically integrates with [`pytest-insta`](https://github.com/vberlier/pytest-insta)
**Hmmkay but why?**
- Editing data packs involves a lot of jumping around between files, for simple use-cases a single file is a lot easier to work with
- Minecraft packs aggregate various types of files that can have complex interactions with each other, a literate style allows you to document these interactions fluently
- Human-readable, single-file data pack and resource pack snapshots can be really useful to diff and track regressions in Minecraft-related tooling
## Installation
The package can be installed with `pip`.
```bash
$ pip install lectern
```
## Getting started
This is an example of a markdown file that can be turned into a data pack:
# Beginner tutorial
Let's start by creating a simple function:
`@function tutorial:greeting`
```mcfunction
say Hello, world!
```
And now we can make it run when the data pack is loaded!
`@function_tag minecraft:load`
```json
{
"values": ["tutorial:greeting"]
}
```
You can use the `lectern` command-line utility to turn the markdown file into a data pack.
```bash
$ lectern tutorial.md --data-pack path/to/tutorial_data_pack
```
If you're using [`beet`](https://github.com/mcbeet/beet) you can use `lectern` as a plugin in your pipeline.
```json
{
"pipeline": ["lectern"],
"meta": {
"lectern": {
"load": ["*.md"]
}
}
}
```
## Document formats
`lectern` implements two closely-related document formats: markdown and plain text. The markdown format builds upon the plain text format.
The markdown format lets you present the various elements of your data pack or resource pack and how they fit together. It's a format that's meant to support [literate programming](https://en.wikipedia.org/wiki/Literate_programming). You can use it when your document is meant to be read by other people. It allows you to emphasize the important parts, explain tradeoffs and discuss alternatives, implementation details, etc...
`@function tutorial:greeting`
```mcfunction
say Hello, world!
```
On the other hand if you don't intend to produce literate documents you can use the plain text format to author data packs and resource packs as a single file without having to deal with markdown formatting.
<!-- @skip -->
```
@function tutorial:greeting
say Hello, world!
```
## Directives
Data pack and resource pack fragments are code blocks, links or images annotated with a special `lectern` directive. Directives are prefixed with the `@` symbol and can be followed by zero or more arguments.
```
@<directive_name> <arg1> <arg2> <arg3>...
```
`lectern` provides directives for including namespaced resources inside data packs and resource packs. These built-in directives all expect a single argument specifying the fully-qualified resource name.
<!-- @skip -->
```
@function tutorial:greeting
@function_tag minecraft:load
```
Here is a reference of all the supported resources:
| Data pack | Resource pack |
| ------------------------------- | ------------------ |
| `@advancement` | `@blockstate` |
| `@function` | `@model` |
| `@loot_table` | `@language` |
| `@predicate` | `@font` |
| `@recipe` | `@glyph_sizes` |
| `@structure` | `@true_type_font` |
| `@block_tag` | `@shader_post` |
| `@entity_type_tag` | `@shader` |
| `@fluid_tag` | `@fragment_shader` |
| `@function_tag` | `@vertex_shader` |
| `@game_event_tag` | `@glsl_shader` |
| `@item_tag` | `@text` |
| `@dimension_type` | `@texture_mcmeta` |
| `@dimension` | `@texture` |
| `@biome` | `@sound` |
| `@configured_carver` | `@particle` |
| `@configured_feature` | |
| `@configured_structure_feature` | |
| `@configured_surface_builder` | |
| `@noise_settings` | |
| `@processor_list` | |
| `@template_pool` | |
| `@item_modifier` | |
> Note that these directives are resolved automatically. If you're working with pack extensions your custom namespaced resources will have their own directives as well.
There are also two built-in directives that can be used to include files using a path relative to the root of the data pack or the resource pack.
<!-- @skip -->
```
@data_pack pack.mcmeta
@resource_pack pack.png
@resource_pack assets/minecraft/textures/block/kelp_plant.png.mcmeta
```
This is useful for adding files that aren't part of any particular namespace.
In case you need to bundle existing resource packs or data packs, you can use the `@merge_zip` directive.
<!-- @skip -->
```
@merge_zip(download)
https://example.com/my_zipped_data_pack.zip
```
Finally, the `@skip` directive is simply ignored and allows you to end a previous fragment in the plain text format.
<!-- @skip -->
```
@function tutorial:greeting
say Hello, world!
@skip
This will not be included in the output.
```
## Code block fragments
You can include the content of a code block in a data pack or a resource pack by preceding it with a directive surrounded by backticks.
`@function tutorial:greeting`
```mcfunction
say Hello, world!
```
You can put the directive in an html comment to make it invisible. Here the code block is annotated with the following comment:
```html
<!-- @function_tag minecraft:load -->
```
<!-- @function_tag minecraft:load -->
```json
{
"values": ["tutorial:greeting"]
}
```
When using backticks you can surround the code block in a `<details>` element to make the code fragment foldable.
`@function tutorial:greeting`
<details>
```mcfunction
say Hello, world!
```
</details>
The directive can also be embedded directly inside the code block. You can insert a directive preceded by either `#` or `//` and the following lines will be included in the specified file.
```mcfunction
# @function tutorial:obtained_dead_bush
say You obtained a dead bush!
```
Embedded directives are stripped from the output. You can use multiple directives in a single code block.
```json
// @loot_table minecraft:blocks/diamond_ore
{
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:dead_bush"
}
]
}
]
}
// @advancement tutorial:obtained_dead_bush
{
"criteria": {
"dead_bush": {
"trigger": "minecraft:inventory_changed",
"conditions": {
"items": [
{
"item": "minecraft:dead_bush"
}
]
}
}
},
"requirements": [
[
"dead_bush"
]
],
"rewards": {
"function": "tutorial:obtained_dead_bush"
}
}
```
It's also possible to use the lectern text format directly inside code blocks.
```mcfunction
@function text_in_block:foo
say foo
```
## Link fragments
Link fragments make it possible to refer to external files, online assets, and to embed binary files in the markdown as data urls. You can create a link fragment by turning a directive surrounded by backticks into a markdown link.
[`@loot_table minecraft:blocks/yellow_shulker_box`](examples/with_links/yellow_shulker_box.json)
The link itself can be a path to a local file or any url supported by the built-in [`urlopen`](https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen) function.
## Image fragments
You can include inline markdown images in the output data pack or resource pack by preceding the image with a directive surrounded by backticks.
`@data_pack pack.png`
![](https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/3a/Pack.png/revision/20210509190015)
Image fragments support the same variations as code block fragments. You can put the directive in a comment or surround the image with a `<details>` element to make it foldable.
## Hidden fragments
You can use html comments to add fragments that will be completely hidden in the rendered markdown.
```html
<!--
@function tutorial:hidden
say This will not appear in the rendered markdown.
@function tutorial:also_hidden
say This is also hidden.
-->
```
<!--
@function tutorial:hidden
say This will not appear in the rendered markdown.
@function tutorial:also_hidden
say This is also hidden.
-->
## Modifiers
The behavior of particular directives can be adjusted with modifiers. A modifier is specified between parentheses right after the name of the directive.
```
@<directive_name>(<modifier>) <arg1> <arg2> <arg3>...
```
The `append` modifier is implemented by all the text-based built-in namespaced resource directives and makes it possible to concatenate the content of the fragment to the already-existing content.
`@function(append) tutorial:greeting`
```mcfunction
say This is added afterwards.
```
You can also use `prepend` to add the fragment before the already-existing content.
`@function(prepend) tutorial:greeting`
```mcfunction
say This is added before.
```
The `merge` modifier is similar but instead of concatenating the contents it uses the `beet` merging strategy to combine the fragment with the existing file.
`@function_tag(merge) minecraft:load`
```json
{
"values": ["#tutorial:something_else"]
}
```
There are also modifiers that are applied to the content of the fragment directly. The `base64` modifier will decode the content of the code fragment as [base64](https://en.wikipedia.org/wiki/Base64).
`@function_tag(base64) tutorial:something_else`
```json
ewogICJ2YWx1ZXMiOiBbInR1dG9yaWFsOnN0cmlwcGVkIl0KfQ==
```
You can use block fragments to download remote files with the `download` modifier.
`@function_tag(download) tutorial:from_github`
```json
https://raw.githubusercontent.com/mcbeet/beet/main/examples/load_basic/src/data/demo/functions/foo.mcfunction
```
Finally, there's a `strip_final_newline` modifier that removes the final newline at the end of code block fragments. It's mostly used to make sure that `lectern` snapshots can reconstruct the original content byte for byte in case the file wasn't terminated by a newline.
`@function(strip_final_newline) tutorial:stripped`
```mcfunction
say This function doesn't have a final newline.
```
## Overlays
You can use the `@overlay` directive to make the following directives apply to a specific pack overlay. Overlays were introduced in [Java Edition 1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2).
`@overlay({"min_inclusive": 16, "max_inclusive": 17}) dummy_overlay`
You can specify the `formats` supported by this overlay as a modifier. From now on, all the directives will apply to the overlay `dummy_overlay`.
`@function tutorial:greeting`
```mcfunction
say Hello from overlay!
```
You can switch to another overlay at any time by using the `@overlay` directive again. To go back to the main pack, use the `@endoverlay` directive.
`@endoverlay`
## Command-line utility
```bash
$ lectern --help
Usage: lectern [OPTIONS] [PATH]...
Literate Minecraft data packs and resource packs.
Options:
-d, --data-pack <path> Extract data pack.
-r, --resource-pack <path> Extract resource pack.
-e, --external-files <path> Emit external files.
-p, --prefetch-urls <path> Prefetch markdown links.
-f, --flat Use the flat markdown format.
-o, --overwrite Overwrite the output pack.
-v, --version Show the version and exit.
-h, --help Show this message and exit.
```
You can extract data packs from markdown files with the `-d/--data-pack` option. If the name ends with `.zip` the generated data pack will be zipped. Multiple markdown files can be merged together into a single data pack.
```bash
$ lectern demo.md --data-pack demo_data_pack
$ lectern demo.md -d demo_data_pack
$ lectern demo.md -d demo_data_pack.zip
$ lectern foo.md bar.md -d demo_data_pack
```
The `-r/--resource-pack` option lets you do exactly the same thing but with resource packs. The two options can be combined to extract a data packs and a resource pack at the same time.
```bash
$ lectern demo.md --resource-pack demo_resource_pack
$ lectern demo.md -r demo_resource_pack
$ lectern demo.md -d demo_data_pack -r demo_resource_pack
```
If you want to overwrite an existing data pack or resource pack you need to specify the `-o/--overwrite` option explicitly.
```bash
$ lectern demo.md --overwrite --data-pack demo_data_pack
```
You can also convert a combination of data packs and resource packs into a single markdown file.
```bash
$ lectern demo_data_pack demo.md
$ lectern demo_data_pack.zip demo.md
$ lectern demo_data_pack demo_resource_pack demo.md
$ lectern foo_data_pack bar_data_pack demo.md
```
The last argument is the name of the generated markdown file. By default, the `lectern` utility will bundle binary files into the markdown file as data urls. You can use the `-e/--external-files` option to dump the binary files in a given directory instead.
```bash
$ lectern demo_data_pack demo.md --external-files files
$ lectern demo_data_pack demo.md -e files
$ lectern demo_data_pack demo.md -e .
```
All these commands also work with plain text files. `lectern` will only use the markdown document format when the filename ends with `.md`.
Finally, you can also use the command-line utility to prefetch markdown urls. The `-p/--prefetch-urls` option can replace the urls in-place or in a copy.
```bash
$ lectern --prefetch-urls demo.md
$ lectern --prefetch-urls demo.md demo_prefetched.md
$ lectern -p demo.md demo_prefetched.md
$ lectern -p demo.md
```
By default, the remote files will be bundled as data urls but you can use the `-e/--external-files` option to dump everything in a given directory.
```bash
$ lectern --prefetch-urls demo.md --external-files files
$ lectern --prefetch-urls demo.md demo_prefetched.md --external-files files
$ lectern -p demo.md demo_prefetched.md -e files
$ lectern -p demo.md -e .
```
## Python API
The API revolves around `Document` objects. A `lectern` document holds a `DataPack` and a `ResourcePack`, as well as a dictionary defining the usable directives. The extractors and serializers are also exposed on the document to make it possible to swap them out with custom ones if needed.
```python
from beet import DataPack, ResourcePack
from lectern import Document
document = Document()
assert document.data == DataPack()
assert document.assets == ResourcePack()
```
The constructor makes it possible to provide existing `DataPack` and `ResourcePack` instances, some initial text or markdown content, or a path from which to load an existing `lectern` document.
```python
Document(data=DataPack(), assets=ResourcePack())
Document(text=...)
Document(markdown=...)
Document(path="path/to/document.md")
```
`Document` instances will compare equal if the underlying data packs and resource packs also compare equal.
You can use the `load` method to read a markdown or a plain text file and update the internal data pack and resource pack with the extracted fragments.
```python
document.load("path/to/document.md")
```
If you already have some text or markdown ready to go, you can use the `add_text` and `add_markdown` methods.
```python
document.add_text(...)
document.add_markdown(...)
```
If the markdown content refers to local files you can specify the directory from which the external files should be loaded from with the `external_files` argument.
```python
document.add_markdown(..., external_files="path/to/directory")
```
If you're handling user input and you don't know which document format is being provided you can use the `add` method and `lectern` will detect if the input is markdown or plain text.
```python
document.add(...)
```
You can use the `get_text` and `get_markdown` methods to serialize the entire content of the internal data pack and resource pack. By default the `get_markdown` method will produce markdown that embeds binary files as data urls. You can enable `emit_external_files` and optionally provide a path prefix to generate a dictionary of associated files instead.
```python
text = document.get_text()
markdown = document.get_markdown()
markdown, external_files = document.get_markdown(emit_external_files=True)
markdown, external_files = document.get_markdown(emit_external_files=True, prefix="path/to/directory")
```
Finally, the `save` method lets you serialize and write the document to a given path. If the filename ends with `.md` the generated markdown will bundle binary files as data urls by default. You can use the `external_files` argument to emit the binary files in the given directory instead.
```python
document.save("path/to/document.txt")
document.save("path/to/document.md")
document.save("path/to/document.md", external_files="path/to/files")
```
## Custom directives
Directives are simply callable objects that receive the document fragment, the resource pack, and the data pack as arguments.
```python
from beet import DataPack, ResourcePack, Function
from lectern import Document, Fragment
def my_directive(fragment: Fragment, assets: ResourcePack, data: DataPack):
num1, num2 = fragment.expect("num1", "num2")
result = int(num1) + int(num2)
data["demo:output_result"] = Function([f"say {result}"])
document = Document()
document.directives["my_directive"] = my_directive
document.add_text("@my_directive 32 10")
assert document.data.functions["demo:output_result"] == Function(["say 42"])
```
The `expect` method allows you to unpack the directive arguments and automatically raises an error if the user didn't specify the arguments properly. You can use the `as_file` method to get the content of the fragment as a specific type of file.
```python
def repeated_function(fragment: Fragment, assets: ResourcePack, data: DataPack):
full_name, count = fragment.expect("full_name", "count")
function = fragment.as_file(Function)
function.lines *= int(count)
data[full_name] = function
```
The `as_file` method will take care of reading the file or downloading it if the directive is used with a link fragment. It will also handle the `base64` and `strip_final_newline` modifiers.
You can handle custom modifiers by checking the content of the `modifier` attribute.
## Fragment loaders
The `Document` object lets you register fragment loaders that intercept and potentially modify fragments before they're handled by directives. The `loaders` attribute is a list of callable objects that receive the fragment and the available directives as arguments. Each loader can then forward the fragment as-is or return a modified fragment. You can also return `None` to drop the fragment.
```python
from typing import Mapping, Optional
from lectern import Directive, Document, Fragment
def handle_ignore_modifier(
fragment: Fragment,
directives: Mapping[str, Directive],
) -> Optional[Fragment]:
if fragment.modifier == "ignore":
return None
return fragment
document = Document()
document.loaders.append(handle_ignore_modifier)
document.add_text("@function(ignore) demo:foo\nsay hello")
assert not document.data
```
## Beet plugin
Using `lectern` as a `beet` plugin makes it possible to combine your markdown files with arbitrary `beet` plugins for further processing. The plugin can load files using the plain text and markdown document formats and emit a snapshot of the `beet` context at the end of the build.
```json
{
"pipeline": ["lectern"],
"meta": {
"lectern": {
"load": ["*.md"],
"snapshot": "out/snapshot.md",
"external_files": "out"
}
}
}
```
You can require the plugin programmatically by using the `lectern` plugin factory.
```python
from beet import Context
from lectern import lectern
def my_plugin(ctx: Context):
ctx.require(
lectern(
load=["*.md"],
snapshot="out/snapshot.md",
external_files="out",
)
)
```
All the configuration is optional. The plugin is a no-op if the `load` or `snapshot` options are not specified.
You can retrieve the `Document` instance with the `inject` method. This is useful for adding custom directives.
```python
from beet import Context, DataPack, ResourcePack, Function
from lectern import Document, Fragment
def hello_directive(ctx: Context):
"""Plugin that defines the `@hello <name>` directive."""
document = ctx.inject(Document)
document.directives["hello"] = hello
def hello(fragment: Fragment, assets: ResourcePack, data: DataPack):
name = fragment.expect("name")
function = data.functions.setdefault("hello:greetings", Function([]))
function.lines.append(f"say Hello, {name}!")
```
It's worth mentioning that `lectern` uses the `beet` cache to avoid downloading link fragments repeatedly and keeping your build snappy, especially in watch mode. If you need to re-download link fragments you can clear the `lectern` cache.
```bash
$ beet cache --clear lectern
```
You can also use a plugin to configure a custom cache timeout if you want to make sure that your assets are re-downloaded periodically.
```python
from beet import Context
def download_every_day(ctx: Context):
ctx.cache["lectern"].timeout(hours=24)
```
## Extra directives
If you're using `lectern` as a `beet` plugin you will be able to use additional directives by adding the corresponding plugins to the `require` option.
```json
{
"require": [
"lectern.contrib.require",
"lectern.contrib.script",
"lectern.contrib.define"
],
"pipeline": ["lectern"],
"meta": {
"lectern": {
"load": ["*.md"]
}
}
}
```
The `lectern.contrib.require` plugin adds a directive that lets you require plugins dynamically.
`@require my_plugins.hello`
The `lectern.contrib.script` plugin adds a directive that renders a fragment with Jinja and interprets the result as `lectern` text.
`@script`
<!-- @skip -->
```
{% for i in range(10) %}
@function demo:script_{{ i }}
say {{ i }}
{% endfor %}
```
Note that using `@script` with the text format requires you to escape the directives in the fragment with an additional `@` symbol.
<!-- @skip -->
```
@script
{% for i in range(10) %}
@@function demo:script_{{ i }}
say {{ i }}
{% endfor %}
```
The `lectern.contrib.define` plugin adds a directive that renders a fragment with Jinja and stores the resulting string as a template global.
`@define(strip_final_newline) math_message`
<!-- @skip -->
```
2 + 2 is {{ 2 + 2 }}
```
## Lectern scripts
The text format is pretty well-suited for writing basic data pack and resource pack generators. You can very easily write scripts that produce lectern syntax that can then be turned into a data pack or a resource pack.
```python
# my_script.py
print("@function tutorial:count")
for i in range(10):
print(f"say {i}")
```
```bash
$ python my_script.py > output.txt
$ lectern output.txt -d my_data_pack
```
The `beet` plugin supports this use-case natively and allows you to run lectern scripts within your pipeline.
```json
// beet.json
{
"pipeline": ["lectern"],
"meta": {
"lectern": {
"scripts": [["python", "my_script.py"]]
}
}
}
```
The `scripts` option lets you specify the command-line arguments for your scripts. The scripts don't even have to be written in Python. As long as the command prints out valid `lectern` syntax you can use any language you want.
## Relative resource locations
The `lectern.contrib.relative_location` plugin uses the `beet` context generator to generate namespaced resources in a default location automatically if you don't specify any namespace.
<!-- @skip -->
```
@function foo
function tutorial:bar
@function bar
say hello
```
You can customize the root of unqualified resource locations by using the `generate_namespace` and `generate_prefix` meta variables. By default, `generate_namespace` is set to the `project_id` and `generate_prefix` is empty.
The plugin works pretty nicely with `beet.contrib.relative_function_path`.
```json
// beet.json
{
"require": ["lectern.contrib.relative_location"],
"pipeline": ["lectern", "beet.contrib.relative_function_path"],
"meta": {
"lectern": {
"load": ["*.md"]
}
}
}
```
<!-- @skip -->
```
@function foo
function ./bar
@function bar
say hello
```
## Using YAML
You can use the `lectern.contrib.yaml_to_json` plugin to author JSON files with YAML. Since YAML is a superset of JSON, you don't have to do anything when you use the plugin. YAML fragments are transparently converted to JSON.
```json
{
"require": ["lectern.contrib.yaml_to_json"],
"pipeline": ["lectern"],
"meta": {
"lectern": {
"load": ["*.md"]
}
}
}
```
<!-- @skip -->
```
@function_tag minecraft:tick
values:
- demo:foo
```
The `@data_pack` and `@resource_pack` directives will also convert the fragment to JSON if the file extension matches `.yml` or `.yaml`.
<!-- @skip -->
```
@resource_pack assets/minecraft/sounds.yml
block.note_block.bit_1:
sounds:
- block/note_block/bit_1
subtitle: subtitles.block.note_block.note
```
## Snapshot testing
A lot of Minecraft tooling involves generating data packs and resource packs. Writing tests for this kind of tooling takes time because you need to painstakingly compare everything that you care about with a reference value. This makes it hard to get good coverage, and then even harder to keep making changes to the code being tested afterwards. You're trading robustness and stability for a shackle that massively slows down development.
That's where snapshot testing comes into play. Snapshot testing allows you to record a reference value and then make sure that your code keeps producing the same results. It provides the necessary tools for reviewing snapshots and updating them as your project evolves.
`lectern` documents are useful as snapshot formats because they represent entire data packs and resource packs in a single file that's human-readable and diff-friendly.
[`pytest-insta`](https://github.com/vberlier/pytest-insta) is an extensible snapshot testing plugin for [`pytest`](https://docs.pytest.org/en/stable/). When it's installed, `lectern` defines three additional snapshot formats.
| Extension | Format description |
| ------------------------- | ---------------------------------------------------------------- |
| `.pack.txt` | Plain text snapshot. |
| `.pack.md` | Markdown snapshot with bundled binary files. |
| `.pack.md_external_files` | Directory with a README.md that refers to external binary files. |
You can use these snapshot formats when comparing `Document` objects with the `snapshot` fixture.
```python
def test_generate(snapshot):
data = generate_some_data_pack()
assert snapshot("pack.txt") == Document(data=data)
```
If you're using the `beet` toolchain, keep in mind that you can get a `Document` instance bound to the context object by using the `inject` method.
```python
def test_generate_with_beet(snapshot):
with run_beet(...) as ctx:
assert snapshot("pack.txt") == ctx.inject(Document)
```
This will save the entire data pack and resource pack in the snapshot. For more details about working with the generated snapshots check out the [`pytest-insta` documentation](https://github.com/vberlier/pytest-insta#command-line-options).
## Contributing
Contributions are welcome. Make sure to first open an issue discussing the problem or the new feature before creating a pull request. The project uses [`poetry`](https://python-poetry.org).
```bash
$ poetry install
```
You can run the tests with `poetry run pytest`.
```bash
$ poetry run pytest
```
The project must type-check with [`pyright`](https://github.com/microsoft/pyright). If you're using VSCode the [`pylance`](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension should report diagnostics automatically. You can also install the type-checker locally with `npm install` and run it from the command-line.
```bash
$ npm run watch
$ npm run check
```
The code follows the [`black`](https://github.com/psf/black) code style. Import statements are sorted with [`isort`](https://pycqa.github.io/isort/).
```bash
$ poetry run isort lectern tests
$ poetry run black lectern tests
$ poetry run black --check lectern tests
```
---
License - [MIT](https://github.com/mcbeet/lectern/blob/main/LICENSE)
Raw data
{
"_id": null,
"home_page": "https://github.com/mcbeet/lectern",
"name": "lectern",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.10",
"maintainer_email": null,
"keywords": "literate-programming, beet, resourcepack, minecraft, datapack",
"author": "Valentin Berlier",
"author_email": "berlier.v@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/6e/29/bc55236615f102192cd14cd3933233717ab51d8fde49b5a4ce857ca08774/lectern-0.34.0.tar.gz",
"platform": null,
"description": "<img align=\"right\" src=\"https://raw.githubusercontent.com/mcbeet/lectern/main/logo.png?sanitize=true\" alt=\"logo\" width=\"76\">\n\n# Lectern\n\n[![GitHub Actions](https://github.com/mcbeet/lectern/workflows/CI/badge.svg)](https://github.com/mcbeet/lectern/actions)\n[![PyPI](https://img.shields.io/pypi/v/lectern.svg)](https://pypi.org/project/lectern/)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lectern.svg)](https://pypi.org/project/lectern/)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n[![Discord](https://img.shields.io/discord/900530660677156924?color=7289DA&label=discord&logo=discord&logoColor=fff)](https://discord.gg/98MdSGMm8j)\n\n> Literate Minecraft data packs and resource packs.\n\n`@function tutorial:greeting`\n\n```mcfunction\nsay Hello, world!\n```\n\n## Introduction\n\nThis markdown file is interspersed with code fragments describing the content of a Minecraft data pack. Using `lectern`, you can turn this single file into an actual data pack that can be loaded into the game.\n\n**Features**\n\n- Turn markdown files into data packs and resource packs\n- Merge resources from several markdown files\n- Convert data packs and resource packs into markdown snapshots\n- Can be used as a [`beet`](https://github.com/mcbeet/beet) plugin\n- Highly extensible with custom directives\n- Automatically integrates with [`pytest-insta`](https://github.com/vberlier/pytest-insta)\n\n**Hmmkay but why?**\n\n- Editing data packs involves a lot of jumping around between files, for simple use-cases a single file is a lot easier to work with\n- Minecraft packs aggregate various types of files that can have complex interactions with each other, a literate style allows you to document these interactions fluently\n- Human-readable, single-file data pack and resource pack snapshots can be really useful to diff and track regressions in Minecraft-related tooling\n\n## Installation\n\nThe package can be installed with `pip`.\n\n```bash\n$ pip install lectern\n```\n\n## Getting started\n\nThis is an example of a markdown file that can be turned into a data pack:\n\n # Beginner tutorial\n\n Let's start by creating a simple function:\n\n `@function tutorial:greeting`\n\n ```mcfunction\n say Hello, world!\n ```\n\n And now we can make it run when the data pack is loaded!\n\n `@function_tag minecraft:load`\n\n ```json\n {\n \"values\": [\"tutorial:greeting\"]\n }\n ```\n\nYou can use the `lectern` command-line utility to turn the markdown file into a data pack.\n\n```bash\n$ lectern tutorial.md --data-pack path/to/tutorial_data_pack\n```\n\nIf you're using [`beet`](https://github.com/mcbeet/beet) you can use `lectern` as a plugin in your pipeline.\n\n```json\n{\n \"pipeline\": [\"lectern\"],\n \"meta\": {\n \"lectern\": {\n \"load\": [\"*.md\"]\n }\n }\n}\n```\n\n## Document formats\n\n`lectern` implements two closely-related document formats: markdown and plain text. The markdown format builds upon the plain text format.\n\nThe markdown format lets you present the various elements of your data pack or resource pack and how they fit together. It's a format that's meant to support [literate programming](https://en.wikipedia.org/wiki/Literate_programming). You can use it when your document is meant to be read by other people. It allows you to emphasize the important parts, explain tradeoffs and discuss alternatives, implementation details, etc...\n\n `@function tutorial:greeting`\n\n ```mcfunction\n say Hello, world!\n ```\n\nOn the other hand if you don't intend to produce literate documents you can use the plain text format to author data packs and resource packs as a single file without having to deal with markdown formatting.\n\n<!-- @skip -->\n\n```\n@function tutorial:greeting\nsay Hello, world!\n```\n\n## Directives\n\nData pack and resource pack fragments are code blocks, links or images annotated with a special `lectern` directive. Directives are prefixed with the `@` symbol and can be followed by zero or more arguments.\n\n```\n@<directive_name> <arg1> <arg2> <arg3>...\n```\n\n`lectern` provides directives for including namespaced resources inside data packs and resource packs. These built-in directives all expect a single argument specifying the fully-qualified resource name.\n\n<!-- @skip -->\n\n```\n@function tutorial:greeting\n@function_tag minecraft:load\n```\n\nHere is a reference of all the supported resources:\n\n| Data pack | Resource pack |\n| ------------------------------- | ------------------ |\n| `@advancement` | `@blockstate` |\n| `@function` | `@model` |\n| `@loot_table` | `@language` |\n| `@predicate` | `@font` |\n| `@recipe` | `@glyph_sizes` |\n| `@structure` | `@true_type_font` |\n| `@block_tag` | `@shader_post` |\n| `@entity_type_tag` | `@shader` |\n| `@fluid_tag` | `@fragment_shader` |\n| `@function_tag` | `@vertex_shader` |\n| `@game_event_tag` | `@glsl_shader` |\n| `@item_tag` | `@text` |\n| `@dimension_type` | `@texture_mcmeta` |\n| `@dimension` | `@texture` |\n| `@biome` | `@sound` |\n| `@configured_carver` | `@particle` |\n| `@configured_feature` | |\n| `@configured_structure_feature` | |\n| `@configured_surface_builder` | |\n| `@noise_settings` | |\n| `@processor_list` | |\n| `@template_pool` | |\n| `@item_modifier` | |\n\n> Note that these directives are resolved automatically. If you're working with pack extensions your custom namespaced resources will have their own directives as well.\n\nThere are also two built-in directives that can be used to include files using a path relative to the root of the data pack or the resource pack.\n\n<!-- @skip -->\n\n```\n@data_pack pack.mcmeta\n@resource_pack pack.png\n@resource_pack assets/minecraft/textures/block/kelp_plant.png.mcmeta\n```\n\nThis is useful for adding files that aren't part of any particular namespace.\n\nIn case you need to bundle existing resource packs or data packs, you can use the `@merge_zip` directive.\n\n<!-- @skip -->\n\n```\n@merge_zip(download)\nhttps://example.com/my_zipped_data_pack.zip\n```\n\nFinally, the `@skip` directive is simply ignored and allows you to end a previous fragment in the plain text format.\n\n<!-- @skip -->\n\n```\n@function tutorial:greeting\nsay Hello, world!\n\n@skip\nThis will not be included in the output.\n```\n\n## Code block fragments\n\nYou can include the content of a code block in a data pack or a resource pack by preceding it with a directive surrounded by backticks.\n\n`@function tutorial:greeting`\n\n```mcfunction\nsay Hello, world!\n```\n\nYou can put the directive in an html comment to make it invisible. Here the code block is annotated with the following comment:\n\n```html\n<!-- @function_tag minecraft:load -->\n```\n\n<!-- @function_tag minecraft:load -->\n\n```json\n{\n \"values\": [\"tutorial:greeting\"]\n}\n```\n\nWhen using backticks you can surround the code block in a `<details>` element to make the code fragment foldable.\n\n`@function tutorial:greeting`\n\n<details>\n\n```mcfunction\nsay Hello, world!\n```\n\n</details>\n\nThe directive can also be embedded directly inside the code block. You can insert a directive preceded by either `#` or `//` and the following lines will be included in the specified file.\n\n```mcfunction\n# @function tutorial:obtained_dead_bush\nsay You obtained a dead bush!\n```\n\nEmbedded directives are stripped from the output. You can use multiple directives in a single code block.\n\n```json\n// @loot_table minecraft:blocks/diamond_ore\n{\n \"pools\": [\n {\n \"rolls\": 1,\n \"entries\": [\n {\n \"type\": \"minecraft:item\",\n \"name\": \"minecraft:dead_bush\"\n }\n ]\n }\n ]\n}\n\n// @advancement tutorial:obtained_dead_bush\n{\n \"criteria\": {\n \"dead_bush\": {\n \"trigger\": \"minecraft:inventory_changed\",\n \"conditions\": {\n \"items\": [\n {\n \"item\": \"minecraft:dead_bush\"\n }\n ]\n }\n }\n },\n \"requirements\": [\n [\n \"dead_bush\"\n ]\n ],\n \"rewards\": {\n \"function\": \"tutorial:obtained_dead_bush\"\n }\n}\n```\n\nIt's also possible to use the lectern text format directly inside code blocks.\n\n```mcfunction\n@function text_in_block:foo\nsay foo\n```\n\n## Link fragments\n\nLink fragments make it possible to refer to external files, online assets, and to embed binary files in the markdown as data urls. You can create a link fragment by turning a directive surrounded by backticks into a markdown link.\n\n[`@loot_table minecraft:blocks/yellow_shulker_box`](examples/with_links/yellow_shulker_box.json)\n\nThe link itself can be a path to a local file or any url supported by the built-in [`urlopen`](https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen) function.\n\n## Image fragments\n\nYou can include inline markdown images in the output data pack or resource pack by preceding the image with a directive surrounded by backticks.\n\n`@data_pack pack.png`\n\n![](https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/3a/Pack.png/revision/20210509190015)\n\nImage fragments support the same variations as code block fragments. You can put the directive in a comment or surround the image with a `<details>` element to make it foldable.\n\n## Hidden fragments\n\nYou can use html comments to add fragments that will be completely hidden in the rendered markdown.\n\n```html\n<!--\n@function tutorial:hidden\nsay This will not appear in the rendered markdown.\n\n@function tutorial:also_hidden\nsay This is also hidden.\n-->\n```\n\n<!--\n@function tutorial:hidden\nsay This will not appear in the rendered markdown.\n\n@function tutorial:also_hidden\nsay This is also hidden.\n-->\n\n## Modifiers\n\nThe behavior of particular directives can be adjusted with modifiers. A modifier is specified between parentheses right after the name of the directive.\n\n```\n@<directive_name>(<modifier>) <arg1> <arg2> <arg3>...\n```\n\nThe `append` modifier is implemented by all the text-based built-in namespaced resource directives and makes it possible to concatenate the content of the fragment to the already-existing content.\n\n`@function(append) tutorial:greeting`\n\n```mcfunction\nsay This is added afterwards.\n```\n\nYou can also use `prepend` to add the fragment before the already-existing content.\n\n`@function(prepend) tutorial:greeting`\n\n```mcfunction\nsay This is added before.\n```\n\nThe `merge` modifier is similar but instead of concatenating the contents it uses the `beet` merging strategy to combine the fragment with the existing file.\n\n`@function_tag(merge) minecraft:load`\n\n```json\n{\n \"values\": [\"#tutorial:something_else\"]\n}\n```\n\nThere are also modifiers that are applied to the content of the fragment directly. The `base64` modifier will decode the content of the code fragment as [base64](https://en.wikipedia.org/wiki/Base64).\n\n`@function_tag(base64) tutorial:something_else`\n\n```json\newogICJ2YWx1ZXMiOiBbInR1dG9yaWFsOnN0cmlwcGVkIl0KfQ==\n```\n\nYou can use block fragments to download remote files with the `download` modifier.\n\n`@function_tag(download) tutorial:from_github`\n\n```json\nhttps://raw.githubusercontent.com/mcbeet/beet/main/examples/load_basic/src/data/demo/functions/foo.mcfunction\n```\n\nFinally, there's a `strip_final_newline` modifier that removes the final newline at the end of code block fragments. It's mostly used to make sure that `lectern` snapshots can reconstruct the original content byte for byte in case the file wasn't terminated by a newline.\n\n`@function(strip_final_newline) tutorial:stripped`\n\n```mcfunction\nsay This function doesn't have a final newline.\n```\n\n## Overlays\n\nYou can use the `@overlay` directive to make the following directives apply to a specific pack overlay. Overlays were introduced in [Java Edition 1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2).\n\n`@overlay({\"min_inclusive\": 16, \"max_inclusive\": 17}) dummy_overlay`\n\nYou can specify the `formats` supported by this overlay as a modifier. From now on, all the directives will apply to the overlay `dummy_overlay`.\n\n`@function tutorial:greeting`\n\n```mcfunction\nsay Hello from overlay!\n```\n\nYou can switch to another overlay at any time by using the `@overlay` directive again. To go back to the main pack, use the `@endoverlay` directive.\n\n`@endoverlay`\n\n## Command-line utility\n\n```bash\n$ lectern --help\nUsage: lectern [OPTIONS] [PATH]...\n\n Literate Minecraft data packs and resource packs.\n\nOptions:\n -d, --data-pack <path> Extract data pack.\n -r, --resource-pack <path> Extract resource pack.\n -e, --external-files <path> Emit external files.\n -p, --prefetch-urls <path> Prefetch markdown links.\n -f, --flat Use the flat markdown format.\n -o, --overwrite Overwrite the output pack.\n -v, --version Show the version and exit.\n -h, --help Show this message and exit.\n```\n\nYou can extract data packs from markdown files with the `-d/--data-pack` option. If the name ends with `.zip` the generated data pack will be zipped. Multiple markdown files can be merged together into a single data pack.\n\n```bash\n$ lectern demo.md --data-pack demo_data_pack\n$ lectern demo.md -d demo_data_pack\n$ lectern demo.md -d demo_data_pack.zip\n$ lectern foo.md bar.md -d demo_data_pack\n```\n\nThe `-r/--resource-pack` option lets you do exactly the same thing but with resource packs. The two options can be combined to extract a data packs and a resource pack at the same time.\n\n```bash\n$ lectern demo.md --resource-pack demo_resource_pack\n$ lectern demo.md -r demo_resource_pack\n$ lectern demo.md -d demo_data_pack -r demo_resource_pack\n```\n\nIf you want to overwrite an existing data pack or resource pack you need to specify the `-o/--overwrite` option explicitly.\n\n```bash\n$ lectern demo.md --overwrite --data-pack demo_data_pack\n```\n\nYou can also convert a combination of data packs and resource packs into a single markdown file.\n\n```bash\n$ lectern demo_data_pack demo.md\n$ lectern demo_data_pack.zip demo.md\n$ lectern demo_data_pack demo_resource_pack demo.md\n$ lectern foo_data_pack bar_data_pack demo.md\n```\n\nThe last argument is the name of the generated markdown file. By default, the `lectern` utility will bundle binary files into the markdown file as data urls. You can use the `-e/--external-files` option to dump the binary files in a given directory instead.\n\n```bash\n$ lectern demo_data_pack demo.md --external-files files\n$ lectern demo_data_pack demo.md -e files\n$ lectern demo_data_pack demo.md -e .\n```\n\nAll these commands also work with plain text files. `lectern` will only use the markdown document format when the filename ends with `.md`.\n\nFinally, you can also use the command-line utility to prefetch markdown urls. The `-p/--prefetch-urls` option can replace the urls in-place or in a copy.\n\n```bash\n$ lectern --prefetch-urls demo.md\n$ lectern --prefetch-urls demo.md demo_prefetched.md\n$ lectern -p demo.md demo_prefetched.md\n$ lectern -p demo.md\n```\n\nBy default, the remote files will be bundled as data urls but you can use the `-e/--external-files` option to dump everything in a given directory.\n\n```bash\n$ lectern --prefetch-urls demo.md --external-files files\n$ lectern --prefetch-urls demo.md demo_prefetched.md --external-files files\n$ lectern -p demo.md demo_prefetched.md -e files\n$ lectern -p demo.md -e .\n```\n\n## Python API\n\nThe API revolves around `Document` objects. A `lectern` document holds a `DataPack` and a `ResourcePack`, as well as a dictionary defining the usable directives. The extractors and serializers are also exposed on the document to make it possible to swap them out with custom ones if needed.\n\n```python\nfrom beet import DataPack, ResourcePack\nfrom lectern import Document\n\ndocument = Document()\nassert document.data == DataPack()\nassert document.assets == ResourcePack()\n```\n\nThe constructor makes it possible to provide existing `DataPack` and `ResourcePack` instances, some initial text or markdown content, or a path from which to load an existing `lectern` document.\n\n```python\nDocument(data=DataPack(), assets=ResourcePack())\nDocument(text=...)\nDocument(markdown=...)\nDocument(path=\"path/to/document.md\")\n```\n\n`Document` instances will compare equal if the underlying data packs and resource packs also compare equal.\n\nYou can use the `load` method to read a markdown or a plain text file and update the internal data pack and resource pack with the extracted fragments.\n\n```python\ndocument.load(\"path/to/document.md\")\n```\n\nIf you already have some text or markdown ready to go, you can use the `add_text` and `add_markdown` methods.\n\n```python\ndocument.add_text(...)\ndocument.add_markdown(...)\n```\n\nIf the markdown content refers to local files you can specify the directory from which the external files should be loaded from with the `external_files` argument.\n\n```python\ndocument.add_markdown(..., external_files=\"path/to/directory\")\n```\n\nIf you're handling user input and you don't know which document format is being provided you can use the `add` method and `lectern` will detect if the input is markdown or plain text.\n\n```python\ndocument.add(...)\n```\n\nYou can use the `get_text` and `get_markdown` methods to serialize the entire content of the internal data pack and resource pack. By default the `get_markdown` method will produce markdown that embeds binary files as data urls. You can enable `emit_external_files` and optionally provide a path prefix to generate a dictionary of associated files instead.\n\n```python\ntext = document.get_text()\nmarkdown = document.get_markdown()\nmarkdown, external_files = document.get_markdown(emit_external_files=True)\nmarkdown, external_files = document.get_markdown(emit_external_files=True, prefix=\"path/to/directory\")\n```\n\nFinally, the `save` method lets you serialize and write the document to a given path. If the filename ends with `.md` the generated markdown will bundle binary files as data urls by default. You can use the `external_files` argument to emit the binary files in the given directory instead.\n\n```python\ndocument.save(\"path/to/document.txt\")\ndocument.save(\"path/to/document.md\")\ndocument.save(\"path/to/document.md\", external_files=\"path/to/files\")\n```\n\n## Custom directives\n\nDirectives are simply callable objects that receive the document fragment, the resource pack, and the data pack as arguments.\n\n```python\nfrom beet import DataPack, ResourcePack, Function\nfrom lectern import Document, Fragment\n\ndef my_directive(fragment: Fragment, assets: ResourcePack, data: DataPack):\n num1, num2 = fragment.expect(\"num1\", \"num2\")\n result = int(num1) + int(num2)\n data[\"demo:output_result\"] = Function([f\"say {result}\"])\n\ndocument = Document()\ndocument.directives[\"my_directive\"] = my_directive\ndocument.add_text(\"@my_directive 32 10\")\nassert document.data.functions[\"demo:output_result\"] == Function([\"say 42\"])\n```\n\nThe `expect` method allows you to unpack the directive arguments and automatically raises an error if the user didn't specify the arguments properly. You can use the `as_file` method to get the content of the fragment as a specific type of file.\n\n```python\ndef repeated_function(fragment: Fragment, assets: ResourcePack, data: DataPack):\n full_name, count = fragment.expect(\"full_name\", \"count\")\n function = fragment.as_file(Function)\n function.lines *= int(count)\n data[full_name] = function\n```\n\nThe `as_file` method will take care of reading the file or downloading it if the directive is used with a link fragment. It will also handle the `base64` and `strip_final_newline` modifiers.\n\nYou can handle custom modifiers by checking the content of the `modifier` attribute.\n\n## Fragment loaders\n\nThe `Document` object lets you register fragment loaders that intercept and potentially modify fragments before they're handled by directives. The `loaders` attribute is a list of callable objects that receive the fragment and the available directives as arguments. Each loader can then forward the fragment as-is or return a modified fragment. You can also return `None` to drop the fragment.\n\n```python\nfrom typing import Mapping, Optional\nfrom lectern import Directive, Document, Fragment\n\ndef handle_ignore_modifier(\n fragment: Fragment,\n directives: Mapping[str, Directive],\n) -> Optional[Fragment]:\n if fragment.modifier == \"ignore\":\n return None\n return fragment\n\ndocument = Document()\ndocument.loaders.append(handle_ignore_modifier)\ndocument.add_text(\"@function(ignore) demo:foo\\nsay hello\")\nassert not document.data\n```\n\n## Beet plugin\n\nUsing `lectern` as a `beet` plugin makes it possible to combine your markdown files with arbitrary `beet` plugins for further processing. The plugin can load files using the plain text and markdown document formats and emit a snapshot of the `beet` context at the end of the build.\n\n```json\n{\n \"pipeline\": [\"lectern\"],\n \"meta\": {\n \"lectern\": {\n \"load\": [\"*.md\"],\n \"snapshot\": \"out/snapshot.md\",\n \"external_files\": \"out\"\n }\n }\n}\n```\n\nYou can require the plugin programmatically by using the `lectern` plugin factory.\n\n```python\nfrom beet import Context\nfrom lectern import lectern\n\ndef my_plugin(ctx: Context):\n ctx.require(\n lectern(\n load=[\"*.md\"],\n snapshot=\"out/snapshot.md\",\n external_files=\"out\",\n )\n )\n```\n\nAll the configuration is optional. The plugin is a no-op if the `load` or `snapshot` options are not specified.\n\nYou can retrieve the `Document` instance with the `inject` method. This is useful for adding custom directives.\n\n```python\nfrom beet import Context, DataPack, ResourcePack, Function\nfrom lectern import Document, Fragment\n\ndef hello_directive(ctx: Context):\n \"\"\"Plugin that defines the `@hello <name>` directive.\"\"\"\n document = ctx.inject(Document)\n document.directives[\"hello\"] = hello\n\ndef hello(fragment: Fragment, assets: ResourcePack, data: DataPack):\n name = fragment.expect(\"name\")\n function = data.functions.setdefault(\"hello:greetings\", Function([]))\n function.lines.append(f\"say Hello, {name}!\")\n```\n\nIt's worth mentioning that `lectern` uses the `beet` cache to avoid downloading link fragments repeatedly and keeping your build snappy, especially in watch mode. If you need to re-download link fragments you can clear the `lectern` cache.\n\n```bash\n$ beet cache --clear lectern\n```\n\nYou can also use a plugin to configure a custom cache timeout if you want to make sure that your assets are re-downloaded periodically.\n\n```python\nfrom beet import Context\n\ndef download_every_day(ctx: Context):\n ctx.cache[\"lectern\"].timeout(hours=24)\n```\n\n## Extra directives\n\nIf you're using `lectern` as a `beet` plugin you will be able to use additional directives by adding the corresponding plugins to the `require` option.\n\n```json\n{\n \"require\": [\n \"lectern.contrib.require\",\n \"lectern.contrib.script\",\n \"lectern.contrib.define\"\n ],\n \"pipeline\": [\"lectern\"],\n \"meta\": {\n \"lectern\": {\n \"load\": [\"*.md\"]\n }\n }\n}\n```\n\nThe `lectern.contrib.require` plugin adds a directive that lets you require plugins dynamically.\n\n`@require my_plugins.hello`\n\nThe `lectern.contrib.script` plugin adds a directive that renders a fragment with Jinja and interprets the result as `lectern` text.\n\n`@script`\n\n<!-- @skip -->\n\n```\n{% for i in range(10) %}\n@function demo:script_{{ i }}\nsay {{ i }}\n{% endfor %}\n```\n\nNote that using `@script` with the text format requires you to escape the directives in the fragment with an additional `@` symbol.\n\n<!-- @skip -->\n\n```\n@script\n{% for i in range(10) %}\n@@function demo:script_{{ i }}\nsay {{ i }}\n{% endfor %}\n```\n\nThe `lectern.contrib.define` plugin adds a directive that renders a fragment with Jinja and stores the resulting string as a template global.\n\n`@define(strip_final_newline) math_message`\n\n<!-- @skip -->\n\n```\n2 + 2 is {{ 2 + 2 }}\n```\n\n## Lectern scripts\n\nThe text format is pretty well-suited for writing basic data pack and resource pack generators. You can very easily write scripts that produce lectern syntax that can then be turned into a data pack or a resource pack.\n\n```python\n# my_script.py\nprint(\"@function tutorial:count\")\n\nfor i in range(10):\n print(f\"say {i}\")\n```\n\n```bash\n$ python my_script.py > output.txt\n$ lectern output.txt -d my_data_pack\n```\n\nThe `beet` plugin supports this use-case natively and allows you to run lectern scripts within your pipeline.\n\n```json\n// beet.json\n{\n \"pipeline\": [\"lectern\"],\n \"meta\": {\n \"lectern\": {\n \"scripts\": [[\"python\", \"my_script.py\"]]\n }\n }\n}\n```\n\nThe `scripts` option lets you specify the command-line arguments for your scripts. The scripts don't even have to be written in Python. As long as the command prints out valid `lectern` syntax you can use any language you want.\n\n## Relative resource locations\n\nThe `lectern.contrib.relative_location` plugin uses the `beet` context generator to generate namespaced resources in a default location automatically if you don't specify any namespace.\n\n<!-- @skip -->\n\n```\n@function foo\nfunction tutorial:bar\n\n@function bar\nsay hello\n```\n\nYou can customize the root of unqualified resource locations by using the `generate_namespace` and `generate_prefix` meta variables. By default, `generate_namespace` is set to the `project_id` and `generate_prefix` is empty.\n\nThe plugin works pretty nicely with `beet.contrib.relative_function_path`.\n\n```json\n// beet.json\n{\n \"require\": [\"lectern.contrib.relative_location\"],\n \"pipeline\": [\"lectern\", \"beet.contrib.relative_function_path\"],\n \"meta\": {\n \"lectern\": {\n \"load\": [\"*.md\"]\n }\n }\n}\n```\n\n<!-- @skip -->\n\n```\n@function foo\nfunction ./bar\n\n@function bar\nsay hello\n```\n\n## Using YAML\n\nYou can use the `lectern.contrib.yaml_to_json` plugin to author JSON files with YAML. Since YAML is a superset of JSON, you don't have to do anything when you use the plugin. YAML fragments are transparently converted to JSON.\n\n```json\n{\n \"require\": [\"lectern.contrib.yaml_to_json\"],\n \"pipeline\": [\"lectern\"],\n \"meta\": {\n \"lectern\": {\n \"load\": [\"*.md\"]\n }\n }\n}\n```\n\n<!-- @skip -->\n\n```\n@function_tag minecraft:tick\nvalues:\n - demo:foo\n```\n\nThe `@data_pack` and `@resource_pack` directives will also convert the fragment to JSON if the file extension matches `.yml` or `.yaml`.\n\n<!-- @skip -->\n\n```\n@resource_pack assets/minecraft/sounds.yml\nblock.note_block.bit_1:\n sounds:\n - block/note_block/bit_1\n subtitle: subtitles.block.note_block.note\n```\n\n## Snapshot testing\n\nA lot of Minecraft tooling involves generating data packs and resource packs. Writing tests for this kind of tooling takes time because you need to painstakingly compare everything that you care about with a reference value. This makes it hard to get good coverage, and then even harder to keep making changes to the code being tested afterwards. You're trading robustness and stability for a shackle that massively slows down development.\n\nThat's where snapshot testing comes into play. Snapshot testing allows you to record a reference value and then make sure that your code keeps producing the same results. It provides the necessary tools for reviewing snapshots and updating them as your project evolves.\n\n`lectern` documents are useful as snapshot formats because they represent entire data packs and resource packs in a single file that's human-readable and diff-friendly.\n\n[`pytest-insta`](https://github.com/vberlier/pytest-insta) is an extensible snapshot testing plugin for [`pytest`](https://docs.pytest.org/en/stable/). When it's installed, `lectern` defines three additional snapshot formats.\n\n| Extension | Format description |\n| ------------------------- | ---------------------------------------------------------------- |\n| `.pack.txt` | Plain text snapshot. |\n| `.pack.md` | Markdown snapshot with bundled binary files. |\n| `.pack.md_external_files` | Directory with a README.md that refers to external binary files. |\n\nYou can use these snapshot formats when comparing `Document` objects with the `snapshot` fixture.\n\n```python\ndef test_generate(snapshot):\n data = generate_some_data_pack()\n assert snapshot(\"pack.txt\") == Document(data=data)\n```\n\nIf you're using the `beet` toolchain, keep in mind that you can get a `Document` instance bound to the context object by using the `inject` method.\n\n```python\ndef test_generate_with_beet(snapshot):\n with run_beet(...) as ctx:\n assert snapshot(\"pack.txt\") == ctx.inject(Document)\n```\n\nThis will save the entire data pack and resource pack in the snapshot. For more details about working with the generated snapshots check out the [`pytest-insta` documentation](https://github.com/vberlier/pytest-insta#command-line-options).\n\n## Contributing\n\nContributions are welcome. Make sure to first open an issue discussing the problem or the new feature before creating a pull request. The project uses [`poetry`](https://python-poetry.org).\n\n```bash\n$ poetry install\n```\n\nYou can run the tests with `poetry run pytest`.\n\n```bash\n$ poetry run pytest\n```\n\nThe project must type-check with [`pyright`](https://github.com/microsoft/pyright). If you're using VSCode the [`pylance`](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension should report diagnostics automatically. You can also install the type-checker locally with `npm install` and run it from the command-line.\n\n```bash\n$ npm run watch\n$ npm run check\n```\n\nThe code follows the [`black`](https://github.com/psf/black) code style. Import statements are sorted with [`isort`](https://pycqa.github.io/isort/).\n\n```bash\n$ poetry run isort lectern tests\n$ poetry run black lectern tests\n$ poetry run black --check lectern tests\n```\n\n---\n\nLicense - [MIT](https://github.com/mcbeet/lectern/blob/main/LICENSE)\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Literate Minecraft data packs and resource packs.",
"version": "0.34.0",
"project_urls": {
"Documentation": "https://github.com/mcbeet/lectern",
"Homepage": "https://github.com/mcbeet/lectern",
"Repository": "https://github.com/mcbeet/lectern"
},
"split_keywords": [
"literate-programming",
" beet",
" resourcepack",
" minecraft",
" datapack"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "00ddc7decb1ff008c0c74f1c99048d9f159981839e85eba337aa377a0b0da25a",
"md5": "17e260b31253ec194a1517037fb76403",
"sha256": "e649d6c58c5e5ef56b2fe9e9acee1738e330d45ea0a7ddca3ec557f444f80f3a"
},
"downloads": -1,
"filename": "lectern-0.34.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "17e260b31253ec194a1517037fb76403",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.10",
"size": 35057,
"upload_time": "2024-07-21T23:02:57",
"upload_time_iso_8601": "2024-07-21T23:02:57.239260Z",
"url": "https://files.pythonhosted.org/packages/00/dd/c7decb1ff008c0c74f1c99048d9f159981839e85eba337aa377a0b0da25a/lectern-0.34.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "6e29bc55236615f102192cd14cd3933233717ab51d8fde49b5a4ce857ca08774",
"md5": "7992289f4942ec2a73f6868cc03cf06d",
"sha256": "33bef09e0d1625a56459e8badfba51e0d493ad822fa119a0b630539d8faa0f49"
},
"downloads": -1,
"filename": "lectern-0.34.0.tar.gz",
"has_sig": false,
"md5_digest": "7992289f4942ec2a73f6868cc03cf06d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.10",
"size": 35792,
"upload_time": "2024-07-21T23:02:59",
"upload_time_iso_8601": "2024-07-21T23:02:59.185104Z",
"url": "https://files.pythonhosted.org/packages/6e/29/bc55236615f102192cd14cd3933233717ab51d8fde49b5a4ce857ca08774/lectern-0.34.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-07-21 23:02:59",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "mcbeet",
"github_project": "lectern",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "lectern"
}