├── lit ├── version.o.md ├── funcs │ ├── get_max_lines.o.md │ ├── write_if_different.o.md │ ├── parse_menu_attrib.o.md │ ├── eat.o.md │ ├── parse_arg_name.o.md │ ├── parse_name.o.md │ ├── main.o.md │ ├── parse_exec.o.md │ ├── intersperse.o.md │ ├── parse_args_str.o.md │ ├── parse_default.o.md │ ├── parse_args.o.md │ ├── parse_arg_name_value.o.md │ ├── split_lines.o.md │ ├── parse_arg_value.o.md │ ├── parse_match.o.md │ └── get_match.o.md ├── class_codeblocks │ ├── run_all_blocks_fn.o.md │ ├── get_code_block.o.md │ ├── main.o.md │ ├── run_block_fn.o.md │ ├── print.o.md │ ├── parse.o.md │ ├── handle_cmd.o.md │ └── expand.o.md ├── class_codeblock │ ├── origin.o.md │ ├── run.o.md │ ├── init.o.md │ ├── main.o.md │ ├── tangle.o.md │ ├── run_return_results.o.md │ ├── info.o.md │ ├── parse.o.md │ └── get_run_cmd.o.md ├── parse.o.md ├── command_mode.o.md ├── imports.o.md ├── globals.o.md ├── main.o.md ├── run_all_tests.o.md ├── test_common.o.md ├── code.o.md ├── intro.o.md ├── interactive_mode.o.md ├── main_code.o.md └── experimental_features.o.md ├── e2e-tests ├── other4.o.md ├── other1 │ ├── other1.o.md │ └── other2 │ │ └── other2.o.md ├── other3 │ └── other3.o.md └── main.o.md ├── LICENSE ├── proj_cmd.o.md ├── e2e-tests.o.md └── README.md /lit/version.o.md: -------------------------------------------------------------------------------- 1 | ``` {name=version} 2 | v0.0.4 3 | ``` 4 | -------------------------------------------------------------------------------- /e2e-tests/other4.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | code_dir_4: ~/code-4 4 | project_name_recurse_4: \@ 5 | --- 6 | -------------------------------------------------------------------------------- /e2e-tests/other1/other1.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | code_dir_1: ~/code-1 4 | project_name_recurse_1: \@ 5 | --- 6 | -------------------------------------------------------------------------------- /e2e-tests/other3/other3.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | code_dir_3: ~/code-3 4 | project_name_recurse_3: \@-3 5 | --- 6 | -------------------------------------------------------------------------------- /e2e-tests/other1/other2/other2.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | code_dir_2: ~/code-2 4 | project_name_recurse_2: \@ 5 | --- 6 | -------------------------------------------------------------------------------- /lit/funcs/get_max_lines.o.md: -------------------------------------------------------------------------------- 1 | # get_max_lines 2 | 3 | Get a line count for each section and return the largest count. Used in CodeBlocks::intersperse: 4 | 5 | ```python {name=get_max_lines} 6 | def get_max_lines(sections): 7 | if sections == []: 8 | return 0 9 | 10 | return max(len(s.splitlines()) for s in sections) 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /lit/class_codeblocks/run_all_blocks_fn.o.md: -------------------------------------------------------------------------------- 1 | # Code Blocks run_all_blocks_fn 2 | 3 | A simple function that runs a function on each CodeBlock. The `fn` arg must be a member of the `CodeBlock` class 4 | and only take on argument. 5 | 6 | ```python {name=codeblocks__run_all_blocks} 7 | def run_all_blocks_fn(self, fn): 8 | for block in self.code_blocks: 9 | fn(block) 10 | ``` 11 | -------------------------------------------------------------------------------- /lit/class_codeblock/origin.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::origin` 2 | 3 | Prints the name of the file this code block was originally parsed from. 4 | 5 | This is primarily useful for debugging or logging purposes, so you can trace a block back to its source file. Also helpful for code editor integration. 6 | 7 | ```python {name=codeblock__origin} 8 | def origin(self): 9 | print(self.origin_file) 10 | ``` 11 | -------------------------------------------------------------------------------- /lit/parse.o.md: -------------------------------------------------------------------------------- 1 | # Parse Code 2 | 3 | To parse literate code files (files that end in `o.md`), we create a `CodeBlocks` object and call the `parse` function. 4 | 5 | OMD parses all `o.md` files in the current directory. It looks for OMD files recursively in any subfolders as well. 6 | 7 | ### @ 8 | 9 | ```python {name=parse} 10 | code_blocks = CodeBlocks() 11 | code_blocks.parse() 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /lit/command_mode.o.md: -------------------------------------------------------------------------------- 1 | # Command Mode 2 | 3 | Command mode is straight forward. We take all arguments to the script except the first one (which will be the name of the script itself) and pass them to the `handle_cmd` function on the `code_blocks` object. We exit the program with the exit code returned from the `handle_cmd` function. 4 | 5 | ### @ 6 | 7 | ```python {name=cmd_exit} 8 | sys.exit(code_blocks.handle_cmd(sys.argv[1:])) 9 | ``` 10 | -------------------------------------------------------------------------------- /lit/imports.o.md: -------------------------------------------------------------------------------- 1 | # Imports 2 | 3 | All the required modules are imported here in one place. 4 | 5 | Organizing them this way keeps things tidy and makes it easy to see at a glance what external tools and libraries `omd` depends on. 6 | 7 | ### 🔗 `@` 8 | 9 | ```python {name=imports} 10 | import glob 11 | import json 12 | import sys 13 | import os 14 | import re 15 | import subprocess 16 | from textwrap import indent 17 | from pathlib import Path 18 | import pypandoc 19 | import uuid 20 | ``` 21 | -------------------------------------------------------------------------------- /lit/globals.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | open_sym: \@< 4 | close_sym: \@> 5 | --- 6 | 7 | # Globals 8 | 9 | This section defines the global data used throughout the `omd` system. 10 | 11 | These constants control the supported languages and the special symbols used to mark substitution points in the source. 12 | 13 | ### 🔗 `@` 14 | 15 | ```python {name=globals} 16 | languages = ["bash", "python", "ruby", "haskell", "racket", "perl", "javascript"] 17 | o_sym = "@" 18 | c_sym = "@" 19 | ``` 20 | -------------------------------------------------------------------------------- /lit/main.o.md: -------------------------------------------------------------------------------- 1 | # Main 2 | 3 | Every good program starts with a `main`—and this one is no exception. 4 | 5 | In Python, the special variable `__name__` is set to `"__main__"` when the file is run directly. This allows us to distinguish between two use cases: 6 | 7 | * Running the file as a **standalone script** (when we do want to run the main logic), and 8 | * Importing the file as a **module** (when we don’t want any top-level code to execute automatically). 9 | 10 | This pattern helps keep `omd` both scriptable and import-friendly. 11 | 12 | --- 13 | 14 | ### 🔗 `@` 15 | 16 | ```python {name=main} 17 | if __name__ == '__main__': 18 | @ 19 | ``` 20 | -------------------------------------------------------------------------------- /lit/class_codeblocks/get_code_block.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlocks::get_code_block` 2 | 3 | This method looks up a named `CodeBlock` within the current `CodeBlocks` instance. 4 | 5 | It returns the **first** block that matches the given `name`, or `None` if no match is found. 6 | 7 | Used throughout the system to resolve references like `@` during expansion or execution. 8 | 9 | --- 10 | 11 | ### 🔗 `@` 12 | 13 | ```python {name=codeblocks__get_code_block} 14 | def get_code_block(self, name): 15 | for block in self.code_blocks: 16 | if block.name == name: 17 | return block 18 | return None 19 | ``` 20 | 21 | Simple, linear, and efficient for small to medium projects. 22 | (If performance ever becomes an issue, it could be backed by a dictionary) 23 | -------------------------------------------------------------------------------- /lit/class_codeblocks/main.o.md: -------------------------------------------------------------------------------- 1 | # Code Blocks 2 | 3 | The `CodeBlocks` class represents a collection of `CodeBlock` objects. 4 | 5 | It acts as the main container and controller for code blocks defined in a literate source file. It orchestrates all the block-level behavior in `omd`. 6 | 7 | Here’s its structure: 8 | 9 | ```python {name=class__codeblocks} 10 | class CodeBlocks: 11 | def __init__(self): 12 | self.code_blocks = [] 13 | @ 14 | @ 15 | @ 16 | @ 17 | @ 18 | @ 19 | @ 20 | @ 21 | ``` 22 | 23 | Each method will be documented in detail in the following files. 24 | -------------------------------------------------------------------------------- /lit/class_codeblock/run.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::run` 2 | 3 | This method executes the expanded command associated with a `CodeBlock`. 4 | 5 | It works by: 6 | 7 | 1. Calling [`self.get_run_cmd()`](get_run_cmd.o.md) to build the full shell command (taking into account language, working directory, Docker, or SSH), 8 | 2. Passing that command to Python’s `subprocess.call()` with `shell=True`. 9 | 10 | This method is triggered when a user runs: 11 | 12 | ```bash 13 | omd run 14 | ``` 15 | 16 | on the command line. 17 | 18 | --- 19 | 20 | ### 🔗 `@` 21 | 22 | ```python {name=codeblock__run} 23 | def run(self): 24 | cmd = self.get_run_cmd() 25 | if cmd is None: 26 | print("Error running command") 27 | return 28 | 29 | return subprocess.call(cmd, shell=True) 30 | ``` 31 | 32 | Simple, direct, and effective. 33 | -------------------------------------------------------------------------------- /lit/run_all_tests.o.md: -------------------------------------------------------------------------------- 1 | # Run all tests 2 | 3 | We could just add to this list in the individual test files and delete this file eventually. 4 | 5 | ```bash {name=all_tests menu=true} 6 | echo '@' 7 | echo '@' 8 | echo '@' 9 | echo '@' 10 | echo '@' 11 | echo '@' 12 | echo '@' 13 | echo '@' 14 | echo '@' 15 | echo '@' 16 | echo '@' 17 | echo '@' 18 | echo '@' 19 | echo '@' 20 | echo '@' 21 | echo '@' 22 | echo '@' 23 | echo '@' 24 | echo '@' 25 | ``` 26 | -------------------------------------------------------------------------------- /lit/funcs/write_if_different.o.md: -------------------------------------------------------------------------------- 1 | # function: write_if_different() 2 | 3 | I wrote this function so that when `omd tangle` is called we don't rewrite files that haven't changed. If we re-write all the files, then the timestamp updates on all files, and then programs that use that timestamp (like `make`) think a bunch of files changed that didn't and a bunch of unneccessary work if initiated. 4 | 5 | ```python {name=write_if_different} 6 | def write_if_different(file_path, new_content): 7 | if os.path.exists(file_path): 8 | with open(file_path, 'r') as file: 9 | current_content = file.read() 10 | 11 | if current_content.rstrip('\n') == new_content: 12 | return 13 | 14 | with open(file_path, 'w') as file: 15 | file.write(new_content) 16 | file.write("\n") # put a newline at the end of the file 17 | file.close() 18 | ``` 19 | -------------------------------------------------------------------------------- /lit/test_common.o.md: -------------------------------------------------------------------------------- 1 | # Common Tests Code 2 | 3 | Each test is a single python file. They use the literate references to import code NOT the import statement. This makes is easy to mock anything you would like. Below are a couple functions that are helpful to use in tests. They are also used through literate reference instead of import. I could do this with an import if I wanted to though. 4 | 5 | ### @ 6 | 7 | ```python {name=test_passed} 8 | print(" PASSED: @!!") 9 | exit(0) 10 | ``` 11 | 12 | ```python {name=omd_assert} 13 | import re 14 | 15 | def omd_assert(expected, got): 16 | if expected != got: 17 | print(f" FAIL: Expected '{expected}', Got '{got}'") 18 | exit(1) 19 | 20 | def omd_assert_regex(expected_regex, got): 21 | res = re.search(expected_regex, got) 22 | if not res: 23 | print(f" FAIL: Expected '{expected_regex}', Got '{got}'") 24 | exit(1) 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /lit/code.o.md: -------------------------------------------------------------------------------- 1 | # The Code 2 | 3 | This is the main code file. 4 | 5 | Yes, everything is defined in a single file—but that's totally OK! With literate programming, the code that gets generated is more like **compiled output**. You’re not meant to read it directly (or even commit it to Git). 6 | 7 | This represents a shift in mindset: the *source of truth* becomes your literate files, not the tangled code they produce. And once you get used to it, it’s a really pleasant way to work. 8 | 9 | ### 🔗 Tangle: `omd` 10 | 11 | ```python {name=omd_file tangle=lit/omd} 12 | #!/usr/bin/env python3 13 | 14 | @ 15 | @ 16 | @ 17 | @ 18 | @ 19 | 20 | @ 21 | ``` 22 | 23 | ### @ deps 24 | 25 | * [`@`](imports.o.md) 26 | * [`@`](globals.o.md) 27 | * [`@`](funcs.o.md) 28 | * [`@`](class_code_block.o.md) 29 | * [`@`](class_code_blocks.o.md) 30 | * [`@`](main.o.md) 31 | -------------------------------------------------------------------------------- /lit/class_codeblocks/run_block_fn.o.md: -------------------------------------------------------------------------------- 1 | # CodeBlocks::run_block_fn 2 | 3 | In the [CodeBlocks](class_code_blocks.o.md) class, is a useful function called `run_block_fn` which takes an identifier and a pointer to a function. If the identifier is a digit, it is treated as an index into the list of blocks that the `CodeBlocks` holds as data. The function will call the function `fn` on the `CodeBlock` located at index `identifier`. Otherwise, it is assumed that the identifier represents the name of a `CodeBlock` and the function will search for the `CodeBlock` with a matching name, and execute `fn` with the matching block as its argument. 4 | 5 | This function get used a lot in the [CodeBlocks::handle_cmd](handle_cmd.o.md) function of the program. 6 | 7 | ```python {name=codeblocks__run_block_fn} 8 | def run_block_fn(self, identifier, fn): 9 | block = self.get_code_block(identifier) 10 | 11 | if block is None: 12 | print("Error: No Matching Code Blocks Found.") 13 | return -1 14 | 15 | return fn(block) 16 | ``` 17 | -------------------------------------------------------------------------------- /lit/intro.o.md: -------------------------------------------------------------------------------- 1 | # Organic Markdown Source 2 | 3 | In the spirit of eating your own dog food, what follows is the `omd` source code written in **literate form**. 4 | 5 | You might be wondering: *How is this possible?* 6 | *How can you write a literate programming tool using the literate style of the tool itself?* 7 | Isn't that a chicken-and-egg problem? 8 | 9 | Let me explain. 10 | 11 | I originally wrote `omd` as a single, monolithic Python script—not in any literate style. After experimenting and tweaking it for a while, it became stable enough that I decided to take a more disciplined approach: documenting and testing it properly. 12 | 13 | And what better way to do that than with **literate programming**? 14 | 15 | So I used that simple, brute-force bootstrapping script, my original `omd`, to reimplement `omd` in the literate style I had envisioned. Eventually, when the source code generated from these literate files was as stable and complete as the original bootstrap script, I started using it to generate the `omd` script itself. 16 | 17 | **OMD is now implemented in OMD.** 18 | 19 | 👉 [View the Code Layout](code.o.md) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 adam-ard 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lit/interactive_mode.o.md: -------------------------------------------------------------------------------- 1 | # Interactive Mode 2 | 3 | Interactive mode is an infinite loop. After printing a command prompt it reads input from the user. After splitting the input into a list based on characters separated by whitespace, it check the first word for a couple simple matches. If the word is `exit`, we break out of the loop and exit the program. If the word is `reload`, we parse the literate files again and continue. Otherwise, we pass the command to `handle_cmd` function to execute a single command. After which we loop back and print another command prompt, and wait for the next command. 4 | 5 | ### @ 6 | 7 | ```python {name=interactive_mode} 8 | while True: 9 | cmd = input("> ") # print prompt 10 | @ 11 | ``` 12 | 13 | Each iteration of the while loop executes the following code: 14 | 15 | ### @ 16 | 17 | ```python {name=handle_cmd} 18 | words = cmd.split(" ") 19 | 20 | if words[0] == "exit": 21 | break 22 | 23 | if words[0] == "reload": 24 | code_blocks = CodeBlocks() 25 | code_blocks.parse() 26 | print("code reloaded") 27 | continue 28 | 29 | code_blocks.handle_cmd(words) 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /lit/class_codeblock/init.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::__init__` 2 | 3 | The `__init__` method initializes a new `CodeBlock` instance by setting default values for all its attributes. 4 | 5 | Each instance variable represents metadata or behavior associated with a single code block parsed from a `.o.md` file. 6 | 7 | ```python {name=codeblock__init} 8 | def __init__(self): 9 | self.origin_file = None # the file this code block was parsed from 10 | self.name = None # the name attribute (if present) 11 | self.code = None # the contents of the code block 12 | self.lang = None # the language of the code block 13 | self.cwd = "." # directory in which the block should execute 14 | self.tangle_file = None # file path to write the block to (if tangled) 15 | self.in_menu = False # whether this block should appear in the omd status menu 16 | self.code_blocks = None # reference to the CodeBlocks object containing this block 17 | self.docker_container = None # optional Docker container to run the block in 18 | self.ssh_host = None # optional SSH host to run the block on 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /lit/class_codeblock/main.o.md: -------------------------------------------------------------------------------- 1 | # Code Block 2 | 3 | There are two main classes in `omd`: `CodeBlock` and `CodeBlocks`. 4 | 5 | As you might guess, a `CodeBlocks` object represents a list of `CodeBlock` objects. Each `CodeBlock` instance holds the parsed information from a single code block in a literate source file. 6 | 7 | Below is the structure of the `CodeBlock` class. We'll go into more detail on each method as we progress through the documentation. 8 | 9 | --- 10 | 11 | ### 🔗 `@` 12 | 13 | ```python {name=class__codeblock} 14 | class CodeBlock: 15 | @ 16 | @ 17 | @ 18 | @ 19 | @ 20 | @ 21 | @ 22 | @ 23 | @ 24 | ``` 25 | 26 | --- 27 | 28 | For reference, in a literate source file (with the `.o.md` extension), code blocks follow the [Pandoc fenced code block](https://pandoc.org/MANUAL.html#fenced-code-blocks) format, like so: 29 | 30 | ```` 31 | ``` {name= ...} 32 | source code 33 | ``` 34 | ```` 35 | 36 | This format allows metadata like `name`, `tangle` to be embedded directly in the code block header. 37 | -------------------------------------------------------------------------------- /proj_cmd.o.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | To build a new `omd` script, run the following. Don't run `omd tangle` because that will also tangle the stuff in `e2e-tests/`, which should be tested using the new `lit/omd` file. Here we get around that by just tangling `omd_file` and nothing else. 4 | 5 | ```bash {name=build-omd menu=true} 6 | rm -f lit/omd 7 | omd tangle omd_file 8 | black lit/omd 9 | chmod u+x lit/omd 10 | ``` 11 | 12 | # Unit Tests 13 | To test the new `omd` script, run the following: 14 | 15 | ```bash 16 | omd run all_tests 17 | ``` 18 | 19 | # e2e tests 20 | 21 | To e2e test the new `omd` script, run the following: 22 | 23 | ```bash 24 | omd run e2e-tests 25 | ``` 26 | 27 | # Diff 28 | 29 | To see the difference between the built script and the system on: 30 | 31 | ```bash {name=diff menu=true} 32 | diff lit/omd `which omd` 33 | ``` 34 | 35 | # Release 36 | 37 | To do an official release. 38 | 39 | - Update the version in the file: `lit\version.o.md`. 40 | - Run `build-omd` 41 | - Run `all_tests` 42 | - Run `e2e-tests` 43 | - If all pass, create a commit with the version update and push. 44 | - Create a new release on the right of the github project page (make sure create a tag with the version as well) 45 | - Attach the `lit/omd` file to the release. 46 | -------------------------------------------------------------------------------- /lit/funcs/parse_menu_attrib.o.md: -------------------------------------------------------------------------------- 1 | # Function: parse_menu_attrib 2 | 3 | To determine in a code_block should appear in the menu the displays when you run `omd status`, omd checks for the `menu` fenced code block attribute: 4 | 5 | `````` 6 | ```{name=say-hello menu=true} 7 | echo "Hello" 8 | ``` 9 | `````` 10 | 11 | When the menu attribute is found (see the parse function), then this function gets call with the attribute value as an argument. When the value is true, or has a string value that is not something that seems false-like, then the following function will return true (otherwise false). 12 | 13 | function body: 14 | 15 | ### @ 16 | 17 | ```python {name=parse_menu_attrib} 18 | def parse_menu_attrib(val): 19 | return str(val).lower() not in ["false", "0", "", "nil", "null", "none"] 20 | ``` 21 | 22 | 23 | # Tests 24 | 25 | Test that the [parse_menu_attrib](f_parse_menu_attrib.o.md) function returns true/false correctly based on the input. 26 | 27 | ```python {name=f_parse_menu_attrib_tests menu=true} 28 | @ 29 | @ 30 | 31 | for val in [True, "true", "True", "asdf", "1", 1]: 32 | omd_assert(True, parse_menu_attrib(val)) 33 | 34 | for val in [False, "False", "false", "0", 0, "", None, "nil", "Nil", "null", "Null", "None"]: 35 | omd_assert(False, parse_menu_attrib(val)) 36 | 37 | @ 38 | ``` 39 | -------------------------------------------------------------------------------- /lit/class_codeblock/tangle.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::tangle` 2 | 3 | This method is called when a `CodeBlock` is **tangled**—meaning its contents are expanded and written out to a file. 4 | 5 | When the user runs: 6 | 7 | ```bash 8 | omd tangle 9 | ``` 10 | 11 | `omd` walks through all `CodeBlock` objects in the `CodeBlocks` container and calls `tangle()` on each one (if applicable). 12 | 13 | --- 14 | 15 | ### Behavior 16 | 17 | * First, the `self.tangle_file` value is expanded. This allows references and substitutions in the filename itself. 18 | * Then, the `self.code` content is expanded. 19 | * Finally, the output is written to the file—but only if the contents have changed, using `write_if_different()`. 20 | 21 | This makes it easy to write dynamic or parameterized file names like: 22 | 23 | ``````markdown 24 | ```bash {tangle=@.sh} 25 | 26 | ``` 27 | `````` 28 | 29 | When tangled, this will generate a file with the value of `project_name` as its name. 30 | 31 | --- 32 | 33 | ### 🔗 `@` 34 | 35 | ```python {name=codeblock__tangle} 36 | def tangle(self): 37 | if self.tangle_file is not None: 38 | tangle_file = self.code_blocks.expand(self.tangle_file) 39 | code = self.code_blocks.expand(self.code) 40 | 41 | write_if_different(tangle_file, code) 42 | return None 43 | ``` 44 | 45 | Short, efficient, and extremely powerful when combined with literate references. 46 | -------------------------------------------------------------------------------- /lit/funcs/eat.o.md: -------------------------------------------------------------------------------- 1 | # Eat functions 2 | 3 | Below are a couple function that are used in parsing. They are used to discard delimiting characters (whitespace and `=`). 4 | 5 | 6 | ```python {name=eat_ws} 7 | def eat_ws(txt): 8 | return txt.lstrip() 9 | ``` 10 | 11 | ```python {name=eat_eq} 12 | def eat_eq(txt): 13 | if len(txt) == 0: 14 | return None 15 | 16 | if txt[0] == "=": 17 | return txt[1:] 18 | return None 19 | ``` 20 | 21 | ## Tests 22 | 23 | ```python {name=eat_tests menu=true} 24 | @ 25 | 26 | @ 27 | @ 28 | 29 | # eat whitespace tests 30 | txt = " asdf" 31 | expected = "asdf" 32 | 33 | omd_assert(expected, eat_ws(txt)) 34 | 35 | txt = " " 36 | expected = "" 37 | 38 | omd_assert(expected, eat_ws(txt)) 39 | 40 | txt = " \t " 41 | expected = "" 42 | 43 | omd_assert(expected, eat_ws(txt)) 44 | 45 | txt = " \n\t asdf" 46 | expected = "asdf" 47 | 48 | omd_assert(expected, eat_ws(txt)) 49 | 50 | # eat eq tests 51 | 52 | txt = "=asdf" 53 | expected = "asdf" 54 | 55 | omd_assert(expected, eat_eq(txt)) 56 | 57 | txt = "=\"" 58 | expected = "\"" 59 | 60 | omd_assert(expected, eat_eq(txt)) 61 | 62 | txt = "" 63 | expected = None 64 | 65 | omd_assert(expected, eat_eq(txt)) 66 | 67 | txt = " = " 68 | expected = None 69 | 70 | omd_assert(expected, eat_eq(txt)) 71 | 72 | txt = "asdf" 73 | expected = None 74 | 75 | omd_assert(expected, eat_eq(txt)) 76 | 77 | @ 78 | ``` 79 | -------------------------------------------------------------------------------- /lit/class_codeblock/run_return_results.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::run_return_results` 2 | 3 | This method runs the expanded command associated with the block’s `self.code` and returns its output as a string. 4 | 5 | Unlike `run()`, which simply executes the command and streams its output to the terminal, this method **captures the result** for further processing. It’s used when referencing a block with the `*` syntax, like: 6 | 7 | ```markdown 8 | @ 9 | ``` 10 | 11 | Here’s how it works: 12 | 13 | 1. It retrieves the fully expanded shell command from [`self.get_run_cmd(args)`](get_run_cmd.o.md), 14 | 2. Executes the command using `subprocess.run()` with `capture_output=True`, 15 | 3. Decodes the output to UTF-8, 16 | 4. Removes one trailing newline (if present), 17 | 5. Returns the result as a string. 18 | 19 | --- 20 | 21 | ### 🔗 `@` 22 | 23 | ```python {name=codeblock__run_return_results} 24 | def run_return_results(self, args={}): 25 | cmd = self.get_run_cmd(args) 26 | if cmd is None: 27 | print("Error running command") 28 | return 29 | 30 | output = subprocess.run(cmd, capture_output=True, shell=True) 31 | out_decode = output.stdout.decode("utf-8") 32 | 33 | # remove at most one newline, if it exists at the end of the output 34 | if len(out_decode) > 0 and out_decode[-1] == "\n": 35 | out_decode = out_decode[:-1] 36 | 37 | return out_decode 38 | ``` 39 | 40 | This method gives `omd` its dynamic capabilities—allowing code block outputs to be embedded directly in other files or strings. 41 | -------------------------------------------------------------------------------- /lit/funcs/parse_arg_name.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function takes a reference string's arguments and return the name of the first argument. For example, the ref string `@`, when the arguments are extracted `one=1 two=2` and passed to `parse_arg_name` will yield two return values: the name for the first argument `one`, and the rest of the string `=1 two=2`. 4 | 5 | If something goes wrong, `None` is returned in the name return value. Here is the function: 6 | 7 | ```python {name=parse_arg_name} 8 | def parse_arg_name(txt): 9 | if txt == "" or txt[0].isspace(): 10 | return None, txt 11 | 12 | name = "" 13 | while len(txt) > 0: 14 | if txt[0].isspace() or txt[0] == "=": 15 | return name, txt 16 | 17 | name += txt[0] 18 | txt = txt[1:] 19 | 20 | return name, txt 21 | ``` 22 | 23 | # Testing 24 | 25 | ```python {name=parse_arg_name_tests menu=true} 26 | @ 27 | 28 | def test(txt, expected_name, expected_rest): 29 | name, rest = parse_arg_name(txt) 30 | if expected_name is not None: 31 | omd_assert(expected_rest, rest) 32 | omd_assert(expected_name, name) 33 | 34 | @ 35 | 36 | test("", None, None) 37 | test("one=1 two=2", "one", "=1 two=2") 38 | test("name=value", "name", "=value") 39 | test("name ", "name", " ") 40 | test(" stuff", None, None) 41 | test("name1=value1", "name1", "=value1") 42 | test("name1 = value1", "name1", " = value1") 43 | test("name1 \t = value1", "name1", " \t = value1") 44 | 45 | @ 46 | ``` 47 | -------------------------------------------------------------------------------- /lit/funcs/parse_name.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | The follow function is used to parse the name of a ref (`@`). It returns two arguments: the name and the rest of the ref string. 4 | 5 | ```python {name=parse_name} 6 | def parse_name(txt): 7 | o_txt = txt 8 | name = "" 9 | while len(txt) > 0: 10 | if len(txt) > 1 and txt[0] == "\\" and txt[1] in ['(', '{']: 11 | name += txt[:2] 12 | txt = txt[2:] 13 | continue 14 | 15 | if txt[0] in ['(', '{']: 16 | break 17 | 18 | name += txt[0] 19 | txt = txt[1:] 20 | return name, txt 21 | ``` 22 | 23 | # Tests 24 | 25 | ```python {name=parse_name_tests menu=true} 26 | @ 27 | 28 | def test(txt, expected_name, expected_rest): 29 | name, rest = parse_name(txt) 30 | omd_assert(expected_name, name) 31 | omd_assert(expected_rest, rest) 32 | 33 | @ 34 | 35 | test("one", "one", "") 36 | test("one'", "one'", "") 37 | test("one*", "one*", "") 38 | test("one_two", "one_two", "") 39 | test("one_two(){}", "one_two", "(){}") 40 | test("one_two()", "one_two", "()") 41 | test("one_two{}", "one_two", "{}") 42 | test("one_two)", "one_two)", "") 43 | test("one_two}", "one_two}", "") 44 | test("one}_two", "one}_two", "") 45 | test("one<_two", "one<_two", "") 46 | test("one>_two", "one>_two", "") 47 | test("one=_two", "one=_two", "") 48 | test('one"_two', 'one"_two', "") 49 | test('one two', 'one two', "") 50 | test('one\\()()', 'one\\()', "()") 51 | test('one\\{}()', 'one\\{}', "()") 52 | 53 | @ 54 | ``` 55 | -------------------------------------------------------------------------------- /lit/main_code.o.md: -------------------------------------------------------------------------------- 1 | # Main Code 2 | 3 | The `main_code` section is where our program gets going. Two modes are available: interactive and command. In interactive mode all the literate code is parsed and then you get dropped into a repl, where you can ask questions. Command mode parses the code, runs a single command, and then exits. Usually command mode is sufficient, because the parsing is fast, but if you had a lot of code, it might be nice to parse it once and run several commands in a row without having to pay the price of parsing over and over. 4 | 5 | The way the code determines which mode to invoke is by the number of command-line arguments. If there are zero args, then OMD invokes interactive mode. Otherwise, it executes a single command in command mode. 6 | 7 | ### @ 8 | 9 | ```python {name=main_code} 10 | @ 11 | 12 | if len(sys.argv) > 1: 13 | @ 14 | 15 | else: 16 | @ 17 | ``` 18 | 19 | # Tests 20 | 21 | Simple test that check that the right mode is executed at the right time: command mode when there are more than one argument, and interactive mode otherwise. Also assures that the program exits with an error code if it fails. 22 | 23 | ```python {name=main_code_tests menu=true} 24 | @ 25 | 26 | class sys: 27 | argv = ["script_name"] 28 | 29 | interactive_mode_ran = False; 30 | @ 31 | 32 | omd_assert(True, interactive_mode_ran) 33 | 34 | sys.argv.append("extra") 35 | sys.argv.append("args") 36 | @ 37 | 38 | omd_assert(False, interactive_mode_ran) 39 | 40 | @ 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /lit/class_codeblock/info.o.md: -------------------------------------------------------------------------------- 1 | # Print Info 2 | 3 | To make it easy to inspect a `CodeBlock`, we implement Python’s special `__repr__` method. This method is automatically called when you pass an object to the `print()` function, or when inspecting it in a debugger or REPL. 4 | 5 | It prints out useful metadata and the expanded code block content in a structured and readable format. 6 | 7 | --- 8 | 9 | ### 🔗 `@` 10 | 11 | ```python {name=codeblock__repr} 12 | def __repr__(self): 13 | out = "CodeBlock(" 14 | if self.name is not None: 15 | out += f"name={self.name}, " 16 | if self.origin_file is not None: 17 | out += f"origin={self.origin_file}, " 18 | if self.docker_container is not None: 19 | out += f"docker={self.code_blocks.expand(self.docker_container)}, " 20 | if self.ssh_host is not None: 21 | out += f"ssh={self.code_blocks.expand(self.ssh_host)}, " 22 | if self.lang is not None: 23 | out += f"lang={self.lang}, " 24 | out += f"dir={self.code_blocks.expand(self.cwd)}, " 25 | if self.in_menu: 26 | out += f"menu={self.in_menu}, " 27 | out += ")\n" 28 | out += f"{{\n{indent(self.code_blocks.expand(self.code), ' ')}\n}}" 29 | return out 30 | ``` 31 | 32 | This output includes: 33 | 34 | * Basic metadata: name, file of origin, language, working directory 35 | * Execution targets: Docker container or SSH host (if specified) 36 | * Whether it appears in the menu 37 | * The fully expanded code block body 38 | 39 | --- 40 | 41 | ### Convenience Method: `info()` 42 | 43 | To simplify printing, we add a small `info()` method that just calls `print(self)`. This is a bit more expressive when used in scripts or interactive sessions. 44 | 45 | --- 46 | 47 | ### 🔗 `@` 48 | 49 | ```python {name=codeblock__info} 50 | def info(self): 51 | print(self) 52 | ``` 53 | -------------------------------------------------------------------------------- /lit/funcs/main.o.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | This section includes all the top-level function definitions used throughout `omd`. 4 | 5 | Some of these are simple utilities (like whitespace trimming or string parsing), while others form the core of how `omd` processes and generates code from literate files. You can think of this as the tool's functional backbone. 6 | 7 | Each function is documented and defined in its own separate file. Here, we’re just wiring them together to build the final output in the correct order. 8 | 9 | --- 10 | 11 | ### 🔗 `@` 12 | 13 | ```python {name=funcs} 14 | @ 15 | @ 16 | @ 17 | @ 18 | @ 19 | @ 20 | @ 21 | @ 22 | @ 23 | @ 24 | @ 25 | @ 26 | @ 27 | @ 28 | @ 29 | @ 30 | @ 31 | @ 32 | ``` 33 | 34 | ### @ used 35 | 36 | - [@](code.o.md) 37 | 38 | ### @ deps 39 | 40 | - [@](get_max_lines.o.md) 41 | - [@](f_write_if_different.o.md) 42 | - [@](f_parse_menu_attrib.o.md) 43 | - [@](intersperse.o.md) 44 | - [@](split_lines.o.md) 45 | - [@](get_match.o.md) 46 | - [@](parse_name.o.md) 47 | - [@](parse_exec.o.md) 48 | - [@](parse_args_str.o.md) 49 | - [@](parse_default.o.md) 50 | - [@](parse_match.o.md) 51 | - [@](eat.o.md) 52 | - [@](eat.o.md) 53 | - [@](parse_arg_name.o.md) 54 | - [@](parse_arg_value.o.md) 55 | - [@](parse_arg_name_value.o.md) 56 | - [@](parse_args.o.md) 57 | - [@](experimental_features.o.md) 58 | -------------------------------------------------------------------------------- /lit/funcs/parse_exec.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function is used to determine if the name has an exec annotation. Meaning that when the ref is expanded it will be replaced with the result of running the text it represents in the language it is annotated to be. The exec annotation is add by placing a `*` directly after the name. For example, the reference `@` will be expanded to `Hello There` for the code block: 4 | 5 | `````` 6 | ```bash {name=say_hello} 7 | echo "Hello There" 8 | ``` 9 | `````` 10 | 11 | Here is the code to check for the exec annotation. It takes the text for the name that has been parsed and checks for the `*` character at the end. I returns three values: the name of the ref without the annotation, a True/False value indicating whether the exec annotation was present, and a True/False value indicating the presence (or absense) of an error in the parsing syntax. 12 | 13 | ```python {name=parse_exec} 14 | def parse_exec(txt): 15 | if len(txt) == 0: 16 | print(f'name has zero length') 17 | return "", False, False 18 | 19 | if txt[-1:] == "*": 20 | if len(txt) == 1: 21 | print(f'name has zero length') 22 | return "", False, False 23 | 24 | return txt[:-1], True, True 25 | 26 | return txt, False, True 27 | ``` 28 | 29 | # Testing 30 | 31 | ```python {name=parse_exec_tests menu=true} 32 | @ 33 | 34 | def test(txt, expected_name, expected_exec, expected_success): 35 | name, exec, success = parse_exec(txt) 36 | if expected_success: 37 | omd_assert(expected_name, name) 38 | omd_assert(expected_exec, exec) 39 | omd_assert(expected_success, success) 40 | 41 | @ 42 | 43 | test("", None, None, False) 44 | test("*", None, None, False) 45 | test("one", "one", False, True) 46 | test("one*", "one", True, True) 47 | test("a*", "a", True, True) 48 | 49 | @ 50 | ``` 51 | -------------------------------------------------------------------------------- /lit/funcs/intersperse.o.md: -------------------------------------------------------------------------------- 1 | # Intersperse 2 | 3 | To be honest, I think intersperse is probably a bad name for this, but I couldn't come up with any thing else. What this function does is takes a list of input strings (conceivably each with multiple lines) and weaves them together, creating a resulting list of N strings, where N is the number of lines in the string that has the most lines. Each line will have one entry from the input string (if one string has less lines than N lines, we'll just repeat the last line.) This is a little hard to explain, so here is an example: 4 | 5 | Input -> ["1\n2\n3", "4\n5", "6"] 6 | Output -> ["146", "256", "356"] 7 | 8 | Actually, the resulting list, right before being returned, get joined by `\n` characters, so a single string will get returned. Like this: "146\n256\n356". Here is the code for the `intersperse` function: 9 | 10 | ```python {name=intersperse} 11 | def intersperse(sections): 12 | out = [] 13 | max_lines = get_max_lines(sections) 14 | for i in range(max_lines): 15 | line = "" 16 | for s in sections: 17 | lines = s.split("\n") 18 | if i < len(lines): 19 | line += lines[i] 20 | else: 21 | line += lines[-1] # repeat the last entry 22 | out.append(line) 23 | return "\n".join(out) 24 | ``` 25 | 26 | # Tests 27 | 28 | Here are a few tests to confirm that the intersperse functionality is working correctly: 29 | 30 | 31 | ```python {name=intersperse_tests menu=true} 32 | @ 33 | @ 34 | @ 35 | 36 | omd_assert(intersperse([]), "") 37 | omd_assert(intersperse(["a", "b"]), "ab") 38 | omd_assert(intersperse(["a\nb", "c\nd"]), "ac\nbd") 39 | omd_assert(intersperse(["a\nb\nc", "d\ne"]), "ad\nbe\nce") 40 | omd_assert(intersperse(["a\nb\nc\nd", "e\nf"]), "ae\nbf\ncf\ndf") 41 | omd_assert(intersperse(["e\nf", "a\nb\nc\nd"]), "ea\nfb\nfc\nfd") 42 | omd_assert(intersperse(["e\nf", ":", "a\nb\nc\nd", ":"]), "e:a:\nf:b:\nf:c:\nf:d:") 43 | 44 | @ 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /lit/funcs/parse_args_str.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function is used to parse out the string that contains all the arguments from a reference string. For example, the ref string `@` would first get parsed for the name and pass the rest of the string `(one=1 two=2){asdf}` to `parse_args_str`. `parse_args_str` would return `one=1 two=2` and the rest of the string as the second return value: `{asdf}`. 4 | 5 | Here is the code used to parse out the args. If there is an error in the parsing, `None` is returned for the args return value. 6 | 7 | ```python {name=parse_args_str} 8 | def parse_args_str(txt): 9 | args = "" 10 | if len(txt) == 0: 11 | return args, txt 12 | 13 | if txt[0] == '{': 14 | return args, txt 15 | 16 | if txt[0] != '(': 17 | print(f'Bad char: {txt[0]} while parsing args from: "{txt}"') 18 | return None, txt 19 | 20 | txt = txt[1:] # eat the opening paren 21 | open_count = 1 22 | while len(txt) > 0: 23 | if txt[0] == '(': 24 | open_count += 1 25 | elif txt[0] == ')': 26 | open_count -= 1 27 | 28 | if open_count < 1: 29 | return args, txt[1:] 30 | 31 | args += txt[0] 32 | txt = txt[1:] 33 | 34 | return None, False 35 | ``` 36 | 37 | # Testing 38 | 39 | ```python {name=parse_args_str_tests menu=true} 40 | @ 41 | 42 | def test(txt, expected_args, expected_rest): 43 | args, rest = parse_args_str(txt) 44 | if expected_args is not None: 45 | omd_assert(expected_rest, rest) 46 | omd_assert(expected_args, args) 47 | 48 | @ 49 | 50 | test("", "", "") 51 | test("(one=1 two=2){asdf}", "one=1 two=2", "{asdf}") 52 | test("{}", "", "{}") 53 | test("aa", None, None) 54 | test("()", "", "") 55 | test("(a=5 b=6)", "a=5 b=6", "") 56 | test("(a=5 b=6)asdf", "a=5 b=6", "asdf") 57 | test('(a="5" b="6")', 'a="5" b="6"', "") 58 | test("(asdf", None, None) 59 | test("(a=5 b=@)", "a=5 b=@", "") 60 | test("(((())))", "((()))", "") 61 | 62 | @ 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /lit/funcs/parse_default.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function takes a reference string, after the name and args have be removed and checks if there is a default value. For example, the ref string `@`, after parsing the name and arguments is `{asdf}`. If you pass that to `parse_default`, it will return the default value `asdf` and the rest of the string which will be empty. I something goes wrong, None will be returned as the first return value. 4 | 5 | ```python {name=parse_default} 6 | def parse_default(txt): 7 | if len(txt) == 0: 8 | return "", txt 9 | 10 | if txt[0] != "{": 11 | print(f'Bad char: {txt[0]} while parsing default from: "{txt}"') 12 | return None, txt 13 | 14 | open_count = 1 15 | default = "" 16 | o_txt = txt 17 | txt = txt[1:] 18 | while len(txt) > 0: 19 | if txt[0] == '{': 20 | open_count += 1 21 | elif txt[0] == '}': 22 | open_count -= 1 23 | 24 | if open_count < 1: 25 | return default, txt[1:] 26 | 27 | default += txt[0] 28 | txt = txt[1:] 29 | 30 | print(f'End of string before getting a "}}" char: "{o_txt}"') 31 | return None, txt 32 | ``` 33 | 34 | # Testing 35 | 36 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 37 | 38 | ```python {name=parse_default_tests menu=true} 39 | o_sym = ":<" 40 | c_sym = ":>" 41 | 42 | @ 43 | 44 | def test(txt, expected_default, expected_rest): 45 | default, rest = parse_default(txt) 46 | if expected_default is not None: 47 | omd_assert(expected_rest, rest) 48 | omd_assert(expected_default, default) 49 | 50 | @ 51 | 52 | test("", "", "") 53 | test("{asdf}", "asdf", "") 54 | test("()", None, None) 55 | test("aa", None, None) 56 | test("{}", "", "") 57 | test("{a}", "a", "") 58 | test("{:}", ":", "") 59 | test("{aasfd", None, None) 60 | test("{:}", ":", "") 61 | test("{{{{{{}}}}}}", "{{{{{}}}}}", "") 62 | 63 | @ 64 | ``` 65 | -------------------------------------------------------------------------------- /lit/funcs/parse_args.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function takes the arguments from a reference string and returns all the name/value pairs in a python dict. For example, the ref string `@` yeilds a string of argument `one=1 two=2` which gets passed to `parse_args`. `parse_args` returns: `{"one":"1", "two":"2"}`. 4 | 5 | This function makes use of [parse_arg_name_value](parse_arg_name_value.o.md). 6 | 7 | ```python {name=parse_args} 8 | def parse_args(txt): 9 | args = {} 10 | while len(txt) > 0: 11 | name, value, txt = parse_arg_name_value(txt) 12 | if name == None: 13 | return {} 14 | if name == "": 15 | return args 16 | args[name] = value 17 | return args 18 | ``` 19 | 20 | # Testing 21 | 22 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 23 | 24 | ```python {name=parse_args_tests menu=true} 25 | o_sym = ":<" 26 | c_sym = ":>" 27 | 28 | @ 29 | 30 | @ 31 | @ 32 | @ 33 | @ 34 | @ 35 | @ 36 | 37 | def test(txt, expected_args): 38 | args = parse_args(txt) 39 | omd_assert(expected_args, args) 40 | 41 | test('', {}) 42 | test('a1=v1 a2=v2', {"a1": "v1", "a2": "v2"}) 43 | test('a1=: a2=v2', {"a1": ":", "a2": "v2"}) 44 | test('a1="v1" a2="v2"', {"a1": "v1", "a2": "v2"}) 45 | test('arg1="val1" arg2="val2"', {"arg1": "val1", "arg2": "val2"}) 46 | test('arg1="val1" arg2="val2"', {"arg1": "val1", "arg2": "val2"}) 47 | test('arg1="val1" arg2="val2"', {"arg1": "val1", "arg2": "val2"}) 48 | test('arg1 = "val1" arg2 = "val2"', {"arg1": "val1", "arg2": "val2"}) 49 | test('arg1="" arg2=""', {"arg1": "", "arg2": ""}) 50 | test('arg1=" " arg2=" "', {"arg1": " ", "arg2": " "}) 51 | test('arg1="val one" arg2="val one"', {"arg1": "val one", "arg2": "val one"}) 52 | test(' arg1 = " val1 " arg2 = " val2 "', {"arg1": " val1 ", "arg2": " val2 "}) 53 | 54 | txt = 'ret_name="parsed_bool" ret="bool value, float one" name="myFunc" args="char *filename, float two"' 55 | test(txt, {"ret_name": "parsed_bool", 56 | "ret": "bool value, float one", 57 | "name": "myFunc", 58 | "args": "char *filename, float two"}) 59 | 60 | 61 | @ 62 | ``` 63 | -------------------------------------------------------------------------------- /lit/funcs/parse_arg_name_value.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function takes the arguments from a reference string and returns the first name/value pair, and the rest of the string. For example, the ref string `@` yeilds a string of argument `one=1 two=2` which gets passed to `parse_arg_name_value`. `parse_arg_name_value` returns three arguments: first arg name `one`, first arg value `1`, and the rest of the string ` two=2`. If something goes wrong, `None` will be returned as the first return value. 4 | 5 | This function makes use of [parse_arg_name](parse_arg_name.o.md), [parse_arg_value](parse_arg_value.o.md), [eat_ws](parse_eat.o.md), and [eat_eq](parse_eat.o.md). 6 | 7 | ```python {name=parse_arg_name_value} 8 | def parse_arg_name_value(txt): 9 | txt = eat_ws(txt) 10 | if txt == "": 11 | return "", "", "" 12 | 13 | name, txt = parse_arg_name(txt) 14 | if name == None: 15 | return None, None, "" 16 | 17 | txt = eat_ws(txt) 18 | txt = eat_eq(txt) 19 | if txt == None: 20 | return None, None, "" 21 | 22 | txt = eat_ws(txt) 23 | value, txt = parse_arg_value(txt) 24 | if value == None: 25 | return None, None, "" 26 | 27 | return name, value, txt 28 | ``` 29 | 30 | # Testing 31 | 32 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 33 | 34 | ```python {name=parse_arg_name_value_tests menu=true} 35 | o_sym = ":<" 36 | c_sym = ":>" 37 | 38 | @ 39 | 40 | def test(txt, expected_name, expected_value, expected_rest): 41 | name, value, rest = parse_arg_name_value(txt) 42 | if expected_name is not None: 43 | omd_assert(expected_value, value) 44 | omd_assert(expected_rest, rest) 45 | omd_assert(expected_name, name) 46 | 47 | @ 48 | @ 49 | @ 50 | @ 51 | @ 52 | 53 | test("", "", "", "") 54 | test("name=val1", "name", "val1", "") 55 | test("name=val1 name2=asdf", "name", "val1", " name2=asdf") 56 | test("name = val1", "name", "val1", "") 57 | test("name \t = \t val1", "name", "val1", "") 58 | test('name = "val1 val2"', "name", "val1 val2", "") 59 | test('name = ": :" name2=asdf', "name", ': :', " name2=asdf") 60 | 61 | @ 62 | ``` 63 | -------------------------------------------------------------------------------- /lit/funcs/split_lines.o.md: -------------------------------------------------------------------------------- 1 | # split_lines 2 | 3 | This function is a slightly modified way of spliting lines so that refs can span multiple lines like this: 4 | 5 | ``` 6 | @ 9 | ``` 10 | 11 | This can be really handy when there are lots of arguments. This function splits lines up anywhere there is a `\n` character, unless the newline character is inbetween the ref symbols (`o_sym` and `c_sym`). Note that I use an int for `ref_num` not a `bool`, so that I can handle cases where there are refs nested inside of refs (see the tests below). 12 | 13 | ```python {name=split_lines} 14 | def split_lines(txt): 15 | new_lines = [] 16 | 17 | ref_num = 0 18 | length = len(txt) 19 | i = 0 20 | start = 0 21 | while i < length: 22 | if txt[i] == "\n" and ref_num == 0: # need a newline 23 | new_lines.append(txt[start:i]) 24 | start=i+1 25 | if txt[i : i + len(o_sym)] == o_sym: 26 | ref_num += 1 27 | i += len(o_sym) - 1 28 | if txt[i : i + len(c_sym)] == c_sym: 29 | ref_num -= 1 30 | i += len(c_sym) - 1 31 | i += 1 32 | 33 | new_lines.append(txt[start:i]) 34 | return new_lines 35 | ``` 36 | 37 | ## Testing 38 | 39 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 40 | 41 | ```python {name=split_lines_tests menu=true} 42 | o_sym = ":<" 43 | c_sym = ":>" 44 | 45 | @ 46 | 47 | @ 48 | 49 | txt = "test" 50 | expected = ["test"] 51 | 52 | omd_assert(expected, split_lines(txt)) 53 | 54 | txt = "a\nb\nc" 55 | expected = ["a", "b", "c"] 56 | 57 | omd_assert(expected, split_lines(txt)) 58 | 59 | txt = "A:\nC\nD" 60 | expected = ["A:", "C", "D"] 61 | 62 | omd_assert(expected, split_lines(txt)) 63 | 64 | # Test that refs nested in refs work 65 | txt = """A:" 67 | b=2):> 68 | C 69 | D""" 70 | expected = ["""A:" 72 | b=2):>""", "C", "D"] 73 | 74 | omd_assert(expected, split_lines(txt)) 75 | 76 | # test asymmetric opening and closing symbols 77 | o_sym = "::::::<" 78 | c_sym = ":::>" 79 | 80 | txt = "A::::::\nC\nD" 81 | expected = ["A::::::", "C", "D"] 82 | 83 | omd_assert(expected, split_lines(txt)) 84 | 85 | @ 86 | ``` 87 | -------------------------------------------------------------------------------- /lit/experimental_features.o.md: -------------------------------------------------------------------------------- 1 | # Experimental Features 2 | 3 | Below are some experimental features that are in development. They are not ready to be used. I am using them to help me develop a good work flow around importing source files into literate source files and generating polished documentation files with lots of bells and whistles (internal linking, expansion of refs, etc..) 4 | 5 | ```python {name=codeblocks__weave_file} 6 | def weave_file(self, filename, dest): 7 | with open(filename, 'r') as f: 8 | content = f.read() 9 | 10 | # Split by triple backticks to find code and non-code sections 11 | parts = re.split(r'(```.*?```)', content, flags=re.DOTALL) 12 | weaved_content = [] 13 | 14 | for part in parts: 15 | if part.startswith("```"): 16 | # Code section - keep as-is 17 | weaved_content.append(part) 18 | else: 19 | # Non-code section 20 | expanded_part = self.expand(part) 21 | weaved_content.append(expanded_part) 22 | 23 | # Write the weaved output to a new Markdown file 24 | weaved_filename = f"{dest}/{filename}" 25 | with open(weaved_filename, 'w') as f: 26 | f.write("".join(weaved_content)) 27 | print(f"Weaved file created: {weaved_filename}") 28 | ``` 29 | 30 | ```python {name=import_file} 31 | def import_file(lang, file_path): 32 | print(f"importing {file_path}") 33 | 34 | # Get the absolute path of the file and the current directory 35 | abs_file_path = os.path.abspath(file_path) 36 | current_directory = os.path.abspath(os.getcwd()) 37 | 38 | # Check if the file path is a descendant of the current directory 39 | if not abs_file_path.startswith(current_directory): 40 | raise ValueError("The file path must be a descendant of the current directory.") 41 | 42 | # Ensure the file exists 43 | if not os.path.isfile(abs_file_path): 44 | raise FileNotFoundError(f"The file '{file_path}' does not exist.") 45 | 46 | # Extract the filename and create the new filename with ".o.md" extension 47 | original_filename = os.path.basename(file_path) 48 | new_filename = f"{original_filename}.o.md" 49 | new_file_path = os.path.join(current_directory, new_filename) 50 | 51 | # Read the content of the original file 52 | with open(abs_file_path, 'r') as original_file: 53 | content = original_file.read() 54 | 55 | # Modify the content by adding triple backticks and the {name=} tag 56 | modified_content = f"```{lang} {{tangle={abs_file_path}}}\n{content}```\n" 57 | 58 | # Write the modified content to the new file in the current directory 59 | with open(new_file_path, 'w') as new_file: 60 | new_file.write(modified_content) 61 | ``` 62 | -------------------------------------------------------------------------------- /lit/funcs/parse_arg_value.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | This function takes the argument string that remains after an argument name is removed from a ref string, and parses the value. For example, the ref string `@`, will yeild the argument list `one=1 two=2`. Next the argument name will be removed and you'll be left with `1 two=2`. If you pass that to `parse_arg_value` it will return `1` for the first return value and the rest of the string for the second return value: ` two=2`. If something goes wrong, `None` is returned in the value return value. Here is the function: 4 | 5 | ```python {name=parse_arg_value} 6 | def parse_arg_value(txt): 7 | if txt == "" or txt[0].isspace(): 8 | return None, txt 9 | 10 | value = "" 11 | quoted = False 12 | in_ref = 0 13 | 14 | if txt[0] == '"': 15 | quoted = True 16 | txt = txt[1:] 17 | 18 | while len(txt) > 0: 19 | if len(txt) > 1 and txt[0] == "\\" and txt[1] in [o_sym[0], c_sym[0], '"']: 20 | value += txt[1:2] 21 | txt = txt[2:] 22 | 23 | if len(txt) >= len(o_sym) and txt[:len(o_sym)] == o_sym: 24 | in_ref += 1 25 | value += o_sym 26 | txt = txt[len(o_sym):] 27 | continue 28 | 29 | if len(txt) >= len(c_sym) and txt[:len(c_sym)] == c_sym: 30 | in_ref -= 1 31 | value += c_sym 32 | txt = txt[len(c_sym):] 33 | continue 34 | 35 | if not quoted and in_ref < 1 and txt[0].isspace(): 36 | return value, txt 37 | 38 | if quoted and in_ref < 1 and txt[0] == '"': 39 | return value, txt[1:] 40 | 41 | value += txt[0] 42 | txt = txt[1:] 43 | 44 | return value, txt 45 | ``` 46 | 47 | # Testing 48 | 49 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 50 | 51 | ```python {name=parse_arg_value_tests menu=true} 52 | o_sym = ":<" 53 | c_sym = ":>" 54 | 55 | @ 56 | 57 | def test(txt, expected_value, expected_rest): 58 | value, rest = parse_arg_value(txt) 59 | if expected_value is not None: 60 | omd_assert(expected_rest, rest) 61 | omd_assert(expected_value, value) 62 | 63 | @ 64 | 65 | test("", None, None) 66 | test(" ", None, None) 67 | 68 | test("1 two=2", "1", " two=2") 69 | test("val1", "val1", "") 70 | test("val1 name2=val2", "val1", " name2=val2") 71 | test('"val1 val2" name2=val2', "val1 val2", " name2=val2") 72 | test('"val1 val2"name2=val2', "val1 val2", "name2=val2") 73 | test(': name2=val2', ":", " name2=val2") 74 | test(': name2=val2', ':', " name2=val2") 75 | test('": :" name2=val2', ': :', " name2=val2") 76 | test("val1\\:< name2=val2", "val1:<", " name2=val2") 77 | test('val1\" name2=val2', 'val1"', " name2=val2") 78 | test('val1\\:> name2=val2', 'val1:>', " name2=val2") 79 | 80 | @ 81 | ``` 82 | -------------------------------------------------------------------------------- /lit/funcs/parse_match.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | The `parse_match` function parses a ref string into its separate parts. For example, the ref string `@`, when passed to the function, would yeild: `{"name": "ref-name", "exec": True, "args": "one=1 two=2", "default": "asdf"}`. If there are any problems parsing the parts of a match, `None` is returned instead a dict. 4 | 5 | ```python {name=parse_match} 6 | def parse_match(txt): 7 | o_txt = txt 8 | name, txt = parse_name(txt) 9 | if name is None: 10 | print(f'Error parsing name from: "{o_txt}"') 11 | return None 12 | 13 | name, exec, success = parse_exec(name) 14 | if success == False: 15 | print(f'Error parsing exec from: "{name}"') 16 | return None 17 | 18 | args, txt = parse_args_str(txt) 19 | if args == None: 20 | print(f'Error parsing args from: "{o_txt}"') 21 | return None 22 | 23 | default, txt = parse_default(txt) 24 | if default == None: 25 | print(f'Error parsing default from: "{o_txt}"') 26 | return None 27 | 28 | return {"name": name, 29 | "exec": exec, 30 | "args": args, 31 | "default": default.strip('"')} 32 | ``` 33 | 34 | # Testing 35 | 36 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 37 | 38 | ```python {name=parse_match_tests menu=true} 39 | o_sym = ":<" 40 | c_sym = ":>" 41 | 42 | @ 43 | 44 | @ 45 | @ 46 | @ 47 | @ 48 | @ 49 | 50 | def test(txt, expected_args): 51 | args = parse_match(txt) 52 | omd_assert(expected_args, args) 53 | 54 | test('', None) 55 | test('one(', None) 56 | test('one)(', None) 57 | test('one()(', None) 58 | test("one", {"name": "one", "exec": False, "args":"", "default": ""}) 59 | test('one(){}', {"name": "one", "exec": False, "args":"", "default": ""}) 60 | test("one*(){}", {"name": 'one', "exec": True, "args": "", "default": ""}) 61 | test('one(){1}', {"name": 'one', "exec": False, "args": '', "default": "1"}) 62 | test("one*(){1}", {"name": 'one', "exec": True, "args": '', "default": "1"}) 63 | test('one(){"1"}', {"name": 'one', "exec": False, "args": '', "default": '1'}) 64 | test("one*(){lots of stuff}", {"name": 'one', "exec": True, "args": '', "default": "lots of stuff"}) 65 | test("one*(a=5 b=6){lots of stuff}", {"name": 'one', "exec": True, "args": 'a=5 b=6', "default": "lots of stuff"}) 66 | test('one(){:}', {"name": 'one', "exec": False, "args": '', "default": ":"}) 67 | test('one(two=":")', {"name": 'one', "exec": False, "args": 'two=":"', "default": ""}) 68 | test('two_sentences(one=":")', {"name": 'two_sentences', "exec": False, "args": 'one=":"', "default": ""}) 69 | test('two_sentences*(one=":")', {"name": 'two_sentences', "exec": True, "args": 'one=":"', "default": ""}) 70 | test('one(arg1="val1")', {"name": 'one', "exec": False, "args": 'arg1="val1"', "default": ""}) 71 | test("one()", {"name": 'one', "exec": False, "args": '', "default": ""}) 72 | test("one", {"name": 'one', "exec": False, "args": '', "default": ""}) 73 | test("one*", {"name": 'one', "exec": True, "args": '', "default": ""}) 74 | test("one*()", {"name": 'one', "exec": True, "args": '', "default": ""}) 75 | test('one*(two=":")', {"name": 'one', "exec": True, "args": 'two=":"', "default": ""}) 76 | test('one*(arg1="val1")', {"name": 'one', "exec": True, "args": 'arg1="val1"', "default": ""}) 77 | 78 | 79 | @ 80 | ``` 81 | -------------------------------------------------------------------------------- /lit/class_codeblock/parse.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::parse` 2 | 3 | This function takes the Pandoc-generated JSON representation of a code block and extracts its metadata and content into the corresponding `CodeBlock` object. 4 | 5 | Pandoc converts a code block like this: 6 | 7 | ````markdown 8 | ```bash {name=test-code-block menu=true} 9 | echo "hello there friend" 10 | ``` 11 | ```` 12 | 13 | Into a JSON structure that, once parsed into Python data, looks something like this: 14 | 15 | ```python 16 | [['', 17 | ['bash'], 18 | [['name', 'test-code-block'], 19 | ['menu', 'true'] 20 | ]], 21 | 'echo "hello there friend"'] 22 | ``` 23 | 24 | Here's how we extract and store that data: 25 | 26 | --- 27 | 28 | ### 🔗 `@` 29 | 30 | ```python {name=codeblock__parse} 31 | def parse(self, the_json): 32 | self.code = the_json[1] 33 | if len(the_json[0][1]) > 0: 34 | self.lang = the_json[0][1][0] 35 | 36 | for attrib in the_json[0][2]: 37 | if attrib[0] == "menu": 38 | self.in_menu = parse_menu_attrib(attrib[1]) 39 | elif attrib[0] == "name": 40 | self.name = attrib[1] 41 | elif attrib[0] == "dir": 42 | self.cwd = attrib[1] 43 | elif attrib[0] == "tangle": 44 | self.tangle_file = attrib[1] 45 | elif attrib[0] == "docker": 46 | self.docker_container = attrib[1] 47 | elif attrib[0] == "ssh": 48 | self.ssh_host = attrib[1] 49 | else: 50 | print(f"Warning: I don't know what attribute this is {attrib[0]}") 51 | ``` 52 | 53 | This parser is intentionally flexible. It gracefully handles unexpected attributes with a warning instead of crashing. 54 | 55 | --- 56 | 57 | ## ✅ Tests for `CodeBlock::parse` 58 | 59 | Here’s a test file that verifies parsing logic across various input scenarios: 60 | 61 | ```python {name=codeblock__parse_tests menu=true} 62 | @ 63 | 64 | def parse_menu_attrib(val): 65 | return val 66 | 67 | class CodeBlockFake: 68 | def __init__(self): 69 | self.code = "" 70 | self.lang = "" 71 | self.in_menu = "" 72 | self.name = "" 73 | self.cwd = "" 74 | self.tangle_file = "" 75 | self.docker_container = "" 76 | self.ssh_host = "" 77 | 78 | def test_fields(self, code, lang, menu, name, dir, tangle_file, docker_container, ssh_host): 79 | omd_assert(code, self.code) 80 | omd_assert(lang, self.lang) 81 | omd_assert(menu, self.in_menu) 82 | omd_assert(name, self.name) 83 | omd_assert(dir, self.cwd) 84 | omd_assert(tangle_file, self.tangle_file) 85 | omd_assert(docker_container, self.docker_container) 86 | omd_assert(ssh_host, self.ssh_host) 87 | 88 | @ 89 | 90 | # No attributes 91 | cb = CodeBlockFake() 92 | cb.parse([['', [], []], ""]) 93 | cb.test_fields("", "", "", "", "", "", "", "") 94 | 95 | # Name and menu 96 | cb = CodeBlockFake() 97 | cb.parse([['', 98 | ['bash'], 99 | [['name', 'test-code-block'], 100 | ['menu', 'true'] 101 | ]], 102 | 'echo "hello there friend"']) 103 | cb.test_fields('echo "hello there friend"', "bash", "true", "test-code-block", "", "", "", "") 104 | 105 | # Menu as boolean 106 | cb = CodeBlockFake() 107 | cb.parse([['', 108 | ['bash'], 109 | [['name', 'test-code-block'], 110 | ['menu', True] 111 | ]], 112 | 'echo "hello there friend"']) 113 | cb.test_fields('echo "hello there friend"', "bash", True, "test-code-block", "", "", "", "") 114 | 115 | # All fields 116 | cb = CodeBlockFake() 117 | cb.parse([["", 118 | ["python"], 119 | [["name", "build_project"], 120 | ["docker", "@"], 121 | ["ssh", "aard@localhost.com"], 122 | ["menu", "true"], 123 | ["dir", "@"], 124 | ["tangle", "@/main.c"] 125 | ]], 126 | "gcc --version"]) 127 | cb.test_fields("gcc --version", "python", "true", "build_project", "@", "@/main.c", "@", "aard@localhost.com") 128 | 129 | @ 130 | ``` 131 | -------------------------------------------------------------------------------- /lit/class_codeblocks/print.o.md: -------------------------------------------------------------------------------- 1 | # Printing Summary & IDE Integration 2 | 3 | The following functions provide output for `omd status` and related tooling. They summarize runnable commands and tangled output files in a clean, human-readable way—or in machine-readable JSON, perfect for editor integration. 4 | 5 | --- 6 | 7 | ### `print_summary()` 8 | 9 | The main entry point for status output—this method prints available commands and output files: 10 | 11 | ```python {name=codeblocks__print_summary} 12 | def print_summary(self): 13 | self.print_cmds() 14 | print("") 15 | self.print_files() 16 | ``` 17 | 18 | --- 19 | 20 | ### `print_cmds()` 21 | 22 | Prints a list of all code blocks marked with `menu=true`, grouped by the file they came from. This is the output shown under “Available commands” in `omd status`. 23 | 24 | ```python {name=codeblocks__print_cmds} 25 | def print_cmds(self): 26 | print("Available commands:") 27 | print(f' (use "omd run " to execute the command)') 28 | origin_file_dict = {} 29 | for num, block in enumerate(self.code_blocks): 30 | if block.origin_file not in origin_file_dict: 31 | origin_file_dict[block.origin_file] = [] 32 | if block.in_menu: 33 | origin_file_dict[block.origin_file].append(block.name) 34 | 35 | for (key, cmd_list) in origin_file_dict.items(): 36 | if cmd_list == []: 37 | continue 38 | print(f" {key}:") 39 | for name in cmd_list: 40 | print(f" {name}") 41 | ``` 42 | 43 | --- 44 | 45 | ### `print_files()` 46 | 47 | Prints a list of all code blocks that produce a file via the `tangle` attribute. This section appears under “Output files” in `omd status`. 48 | 49 | * If the block has no name, it shows the relative file path. 50 | * If it does have a name, it shows the name instead. 51 | 52 | ```python {name=codeblocks__print_files} 53 | def print_files(self): 54 | print("Output files:") 55 | print(f' (use "omd tangle" to generate output files)') 56 | for num, block in enumerate(self.code_blocks): 57 | if block.tangle_file is not None: 58 | if block.name is None or block.name == "": 59 | expanded_filename = self.expand(block.tangle_file) 60 | rel_path = os.path.relpath(expanded_filename) 61 | print(f" {rel_path}") 62 | else: 63 | print(f" {block.name}") 64 | ``` 65 | 66 | --- 67 | 68 | ### `print_parseable_cmds()` 69 | 70 | Prints a compact JSON structure of commands grouped by origin file. This is useful for integrating with tools like: 71 | 72 | * IDE menus 73 | * LSPs 74 | * External runners / dashboards 75 | 76 | ```json 77 | [ 78 | { 79 | "file": "src/foo.o.md", 80 | "cmds": ["build", "test"] 81 | }, 82 | ... 83 | ] 84 | ``` 85 | 86 | ```python {name=codeblocks__print_parseable_cmds} 87 | def print_parseable_cmds(self): 88 | origin_file_dict = {} 89 | for num, block in enumerate(self.code_blocks): 90 | if block.origin_file not in origin_file_dict: 91 | origin_file_dict[block.origin_file] = [] 92 | if block.in_menu: 93 | origin_file_dict[block.origin_file].append(block.name) 94 | 95 | print("[") 96 | first = True # Track if this is the first JSON object 97 | for key, cmd_list in origin_file_dict.items(): 98 | if not cmd_list: 99 | continue 100 | if not first: # Print a comma before the next JSON object 101 | print(",") 102 | first = False # Subsequent iterations are no longer the first 103 | print(f' {{"file": "{key}", "cmds": [', end="") 104 | 105 | first_2 = True 106 | for name in cmd_list: 107 | if not first_2: # Print a comma before the next JSON object 108 | print(",", end="") 109 | first_2 = False 110 | print(f'"{name}"', end="") 111 | print(']}', end="") 112 | print("\n]") 113 | ``` 114 | 115 | ### print commands 116 | 117 | ```python {name=codeblocks__print} 118 | @ 119 | @ 120 | @ 121 | @ 122 | ``` 123 | -------------------------------------------------------------------------------- /lit/funcs/get_match.o.md: -------------------------------------------------------------------------------- 1 | # Source 2 | 3 | `get_match` returns the next match (or None if there isn't one) and whether or not it's a string execution replacement. It will return matches in a left to right order. 4 | 5 | 6 | ```python {name=get_match} 7 | def get_match(txt): 8 | cur = 0 9 | while cur < len(txt): 10 | res, cur = get_match_inner(txt, cur) 11 | if res is not None: 12 | return res 13 | return None 14 | 15 | def get_match_inner(txt, cur): 16 | open_count = 0 17 | start = -1 18 | 19 | while cur < len(txt): 20 | if cur + len(o_sym) <= len(txt) and txt[cur : cur + len(o_sym)] == o_sym: 21 | if start == -1: 22 | start = cur 23 | open_count += 1 24 | cur += len(o_sym) - 1 25 | 26 | elif cur + len(c_sym) <= len(txt) and txt[cur:cur+len(c_sym)] == c_sym: 27 | if start != -1: 28 | open_count -= 1 29 | cur += len(c_sym) - 1 30 | 31 | if open_count < 1 and start != -1: 32 | match = parse_match(txt[start + len(o_sym) : cur - 1]) 33 | if match is None: 34 | print(f"content internal to {o_sym} and {c_sym} is invalid: {txt[start:cur + len(c_sym) - 1]}") 35 | return None, start + len(o_sym) 36 | return match | {"full": txt[start:cur + len(c_sym) - 1], 37 | "start": start, 38 | "end": cur + len(c_sym) - 1}, start + len(o_sym) 39 | cur += 1 40 | 41 | if start == -1: 42 | return None, len(txt) 43 | 44 | return None, start + len(o_sym) 45 | ``` 46 | 47 | # Testing 48 | 49 | Here I swap out the `@<` and `@>` characters for the `:<` and `:>` characters. This drastically simplifies the test code. Otherwise I have to worry about escaping to avoid unwanted code substitutions that will happen during the tangle step. 50 | 51 | ```python {name=get_match_tests menu=true} 52 | o_sym = ":<" 53 | c_sym = ":>" 54 | 55 | @ 56 | 57 | @ 58 | @ 59 | @ 60 | @ 61 | @ 62 | @ 63 | 64 | def test_error(txt): 65 | match = get_match(txt) 66 | omd_assert(None, match) 67 | 68 | def test(txt, start, full, name, exec, args, default): 69 | match = get_match(txt) 70 | omd_assert({"start": start, 71 | "end": start + len(full), 72 | "full": full, 73 | "name": name, 74 | "exec": exec, 75 | "args": args, 76 | "default": default}, match) 77 | 78 | def test_one(txt, start, name, exec, args, default): 79 | match = get_match(txt) 80 | omd_assert({"start": start, 81 | "end": start + len(txt), 82 | "full": txt, 83 | "name": name, 84 | "exec": exec, 85 | "args": args, 86 | "default": default}, match) 87 | 88 | test_error("asdf:asdf") 89 | test_error('') 90 | test_error(':'), 91 | test_error(':') 92 | test_error(':') 93 | test_error(':') 94 | test_error(':') 95 | 96 | test(":asdf:", 0, ":", "one", False, "", "") 97 | test("asdf:asdf", 4, ":", "one", False, "", "") 98 | test(":asdf:", 0, ":", "one", True, "", "") 99 | test("asdf:asdf", 4, ":", "one", True, "", "") 100 | test("asdf:asdf:asdf", 30, ":", "asdf", False, "", "") 101 | 102 | test_one(':', 0, 'one', False, '', '') 103 | test_one(':', 0, 'one', False, '', '') 104 | test_one(':', 0, 'one', True, '', '') 105 | test_one(':', 0, 'one', False, 'arg1="val1"', '') 106 | test_one(':', 0, 'one', True, 'arg1="val1"', '') 107 | test_one(':', 0, 'one', False, '', '1') 108 | test_one(':', 0, 'one', True, '', '1') 109 | test_one(':', 0, 'one', True, '', 'lots of stuff') 110 | test_one(':}:>', 0, 'one', True, '', ':') 111 | test_one(':"):>', 0, 'one', True, 'two=":"', '') 112 | test_one(':"):>', 0, 'two_sentences', False, 'one=":"', '') 113 | test_one(':"):>', 0, 'one', True, 'two=":"', '') 114 | 115 | @ 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /lit/class_codeblock/get_run_cmd.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlock::get_run_cmd` 2 | 3 | This method returns the shell command needed to execute the expanded contents of `self.code`, according to the block’s declared language and execution context. 4 | 5 | It is used internally by `omd run ` and also for inline substitution with `@`. 6 | 7 | If the block is configured to run in a Docker container or over SSH, this method will wrap the command accordingly using `docker exec -it` or `ssh -t` to ensure an interactive terminal session. 8 | 9 | --- 10 | 11 | ### 🔗 `@` 12 | 13 | ```python {name=codeblock__get_run_cmd} 14 | def get_run_cmd(self, args={}): 15 | code = self.code_blocks.expand(self.code, args) 16 | lang = self.lang 17 | 18 | # Decide file extension and interpreter 19 | lang_info = { 20 | "bash": (".sh", "bash"), 21 | "python": (".py", "python3"), 22 | "ruby": (".rb", "ruby"), 23 | "haskell": (".hs", "runhaskell"), 24 | "racket": (".rkt", "racket"), 25 | "perl": (".pl", "perl"), 26 | "javascript": (".js", "node"), 27 | } 28 | 29 | if lang not in lang_info: 30 | print(f"language {lang} is not supported for execution") 31 | return None 32 | 33 | ext, interpreter = lang_info[lang] 34 | 35 | # Generate consistent UUID-based temp file name in /tmp 36 | uid = uuid.uuid4().hex 37 | filename = f"omd-temp-{uid}{ext}" 38 | tmp_path = f"/tmp/{filename}" 39 | remote_path = tmp_path 40 | 41 | with open(tmp_path, "w") as tmp_file: 42 | tmp_file.write(code) 43 | 44 | cwd = self.code_blocks.expand(self.cwd, args) if hasattr(self, "cwd") and self.cwd else None 45 | cd_prefix = f"cd {cwd} && " if cwd else "" 46 | 47 | if self.docker_container is not None: 48 | container = self.code_blocks.expand(self.docker_container, args) 49 | return f"docker cp {tmp_path} {container}:{remote_path} && docker exec {container} bash -c \"{cd_prefix}{interpreter} {remote_path}\" && docker exec {container} rm {remote_path}" 50 | 51 | elif self.ssh_host is not None: 52 | ssh = self.code_blocks.expand(self.ssh_host, args) 53 | return f"scp {tmp_path} {ssh}:{remote_path} && ssh {ssh} '{cd_prefix}{interpreter} {remote_path}' && ssh {ssh} rm {remote_path}" 54 | 55 | else: 56 | return f"{cd_prefix}{interpreter} {tmp_path}" 57 | ``` 58 | 59 | --- 60 | 61 | # Tests 62 | 63 | Here are a few tests to confirm that the get_run_cmd functionality is working correctly: 64 | 65 | ```python {name=get_run_cmd_tests menu=true} 66 | import os 67 | import uuid 68 | 69 | @ 70 | 71 | class CodeBlocksFake: 72 | def expand(self, code, args): 73 | return code 74 | 75 | class CodeBlockFake: 76 | def __init__(self, lang, code, docker_container, ssh_host, cwd): 77 | self.code = code 78 | self.lang = lang 79 | self.code_blocks = CodeBlocksFake() 80 | self.docker_container = docker_container 81 | self.ssh_host = ssh_host 82 | self.cwd = cwd 83 | 84 | def omd_assert(self, exp): 85 | omd_assert(exp, self.get_run_cmd()) 86 | 87 | def omd_assert_regex(self, regex_exp): 88 | omd_assert_regex(regex_exp, self.get_run_cmd()) 89 | 90 | @ 91 | 92 | ### when there is an invalid language specified 93 | CodeBlockFake("not_a_language", "", None, None, "/the/path").omd_assert(None) 94 | 95 | ### when there is no language specified 96 | CodeBlockFake(None, "", None, None, "/the/path").omd_assert(None) 97 | 98 | #### When there is no cwd 99 | CodeBlockFake("bash", "", None, None, None).omd_assert_regex(r"bash /tmp/[^\s]+\.sh") 100 | 101 | #### normal local 102 | CodeBlockFake("bash", "", None, None, "/the/path").omd_assert_regex("cd /the/path && bash /tmp/[^\s]+\.sh") 103 | 104 | ### docker 105 | CodeBlockFake("bash", "", "my_docker", None, "/the/path").omd_assert_regex(r"""docker cp /tmp/[^\s]+\.sh my_docker:/tmp/[^\s]+\.sh && docker exec my_docker bash -c "cd /the/path && bash /tmp/[^\s]+\.sh" && docker exec my_docker rm /tmp/omd-[^\s]+\.sh""") 106 | 107 | #### docker no cwd 108 | CodeBlockFake("bash", "", "my_docker", None, None).omd_assert_regex(r"""docker cp /tmp/[^\s]+\.sh my_docker:/tmp/[^\s]+\.sh && docker exec my_docker bash -c "bash /tmp/[^\s]+\.sh" && docker exec my_docker rm /tmp/omd-[^\s]+\.sh""") 109 | 110 | #### different language 111 | CodeBlockFake("perl", "", None, None, "/the/path").omd_assert_regex(r"""cd /the/path && perl /tmp/[^\s]+\.pl""") 112 | 113 | #### ssh 114 | CodeBlockFake("ruby", "", None, "aard@my-host.com", "/the/other/path").omd_assert_regex(r"""scp /tmp/[^\s]+\.rb aard@my-host.com:/tmp/[^\s]+\.rb && ssh aard@my-host.com 'cd /the/other/path && ruby /tmp/[^\s]+\.rb' && ssh aard@my-host.com rm /tmp/[^\s]+\.rb""") 115 | 116 | @ 117 | ``` 118 | -------------------------------------------------------------------------------- /lit/class_codeblocks/parse.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlocks::parse` 2 | 3 | This method walks the current directory tree, locates all `.o.md` files, and uses Pandoc to parse them into JSON. That JSON is then translated into Python data structures so that each code block—and any constants defined in YAML—can be loaded into the system. 4 | 5 | --- 6 | 7 | ### Example Input 8 | 9 | Here’s a simple `.o.md` file that includes YAML metadata, a heading, a paragraph, and a code block: 10 | 11 | ``````markdown 12 | --- 13 | constants: 14 | test1: "one" 15 | test2: "two" 16 | --- 17 | 18 | # Test file heading 19 | 20 | Here is some explanation 21 | 22 | ```bash {name=test-code-block menu=true lang=python} 23 | echo "hello there friend" 24 | echo "hello there friend2" 25 | ``` 26 | `````` 27 | 28 | This would be converted by Pandoc into JSON, and from there into Python structures like the following: 29 | 30 | ```python 31 | {'blocks': [{'c': [1, 32 | ['test-file-heading', [], []], 33 | [{'c': 'Test', 't': 'Str'}, 34 | {'t': 'Space'}, 35 | {'c': 'file', 't': 'Str'}, 36 | {'t': 'Space'}, 37 | {'c': 'heading', 't': 'Str'}]], 38 | 't': 'Header'}, 39 | {'c': [{'c': 'Here', 't': 'Str'}, 40 | {'t': 'Space'}, 41 | {'c': 'is', 't': 'Str'}, 42 | {'t': 'Space'}, 43 | {'c': 'some', 't': 'Str'}, 44 | {'t': 'Space'}, 45 | {'c': 'explanation', 't': 'Str'}], 46 | 't': 'Para'}, 47 | {'c': [['', 48 | ['bash'], 49 | [['name', 'test-code-block'], 50 | ['menu', 'true'], 51 | ['lang', 'python']]], 52 | 'echo "hello there friend"\necho "hello there friend2"'], 53 | 't': 'CodeBlock'}], 54 | 'meta': {'constants': {'c': {'test1': {'c': [{'c': 'one', 't': 'Str'}], 55 | 't': 'MetaInlines'}, 56 | 'test2': {'c': [{'c': 'two', 't': 'Str'}], 57 | 't': 'MetaInlines'}}, 58 | 't': 'MetaMap'}}, 59 | 'pandoc-api-version': [1, 23, 1]} 60 | ``` 61 | 62 | --- 63 | 64 | ### Parsing Logic 65 | 66 | The code below does the following: 67 | 68 | * **`parse()`**: Recursively searches for `.o.md` files in the current directory and its subdirectories. 69 | * **`parse_file()`**: Converts a Markdown file into Pandoc JSON. 70 | * **`parse_json()`**: 71 | 72 | * Loads constants from the YAML metadata block into `CodeBlock` objects. 73 | * Parses each actual code block into a `CodeBlock` object. 74 | * **`add_code_block()`**: 75 | 76 | * Merges contents if multiple blocks share the same name (concatenating their code). 77 | 78 | --- 79 | 80 | ### 🔗 `@` 81 | 82 | ```python {name=codeblocks__parse} 83 | def parse(self): 84 | # Read all files in the current directory (recursively) with .o.md extension 85 | for root, dirs, files in os.walk("."): 86 | for cur_file in files: 87 | cur_full_file = f"{root}/{cur_file}" 88 | if cur_full_file.endswith(".o.md"): 89 | self.parse_file(cur_full_file) 90 | 91 | def parse_file(self, filename): 92 | data = json.loads(pypandoc.convert_file(filename, 'json', format="md")) 93 | self.parse_json(data, filename) 94 | 95 | def add_code_block(self, code_block): 96 | if code_block.name is not None: 97 | blk = self.get_code_block(code_block.name) 98 | if blk is not None: 99 | code_block.code = blk.code + "\n" + code_block.code 100 | self.code_blocks.remove(blk) 101 | 102 | self.code_blocks.append(code_block) 103 | 104 | def parse_json(self, data, origin_file): 105 | for section, constants in data['meta'].items(): 106 | if section == "constants": 107 | for key, val in constants['c'].items(): 108 | str = "" 109 | for i in val['c']: 110 | if i['t'] == "Str": 111 | str += i['c'] 112 | if i['t'] == "Space": 113 | str += " " 114 | 115 | cb = CodeBlock() 116 | cb.origin_file = origin_file 117 | cb.name = key 118 | cb.code = str 119 | cb.code_blocks = self 120 | 121 | # Append to existing block with same name if needed 122 | self.add_code_block(cb) 123 | 124 | for block in data['blocks']: 125 | if block['t'] == "CodeBlock": 126 | cb = CodeBlock() 127 | cb.origin_file = origin_file 128 | cb.parse(block['c']) 129 | cb.code_blocks = self 130 | 131 | # Append to existing block with same name if needed 132 | self.add_code_block(cb) 133 | ``` 134 | -------------------------------------------------------------------------------- /lit/class_codeblocks/handle_cmd.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlocks::handle_cmd` 2 | 3 | The `handle_cmd()` method is the central dispatcher for command-line interactions with `omd`. It is used in both **interactive mode** and **scripted mode** (e.g., via `omd run `). 4 | 5 | It parses and routes input words (typically from `sys.argv`) to the appropriate method based on how many arguments are provided. 6 | 7 | --- 8 | 9 | ### 🔗 `@` 10 | 11 | ```python {name=codeblocks__handle_cmd} 12 | def handle_cmd(self, words): 13 | if len(words) == 1: 14 | @ 15 | 16 | elif len(words) == 2: 17 | @ 18 | 19 | elif len(words) > 2: 20 | @ 21 | 22 | else: 23 | print("missing cmd") 24 | ``` 25 | 26 | --- 27 | 28 | ## 🧩 One-Word Commands 29 | 30 | These commands are run with just a single word like `status` or `tangle`. 31 | 32 | ### 🔗 `@` 33 | 34 | ```python {name=handle_one_word_commands} 35 | if words[0] == "version": 36 | print("@") 37 | elif words[0] == "cmds": 38 | self.print_parseable_cmds() 39 | elif words[0] == "files": 40 | self.print_files() 41 | elif words[0] == "status": 42 | self.print_summary() 43 | elif words[0] == "tangle": 44 | self.run_all_blocks_fn(CodeBlock.tangle) 45 | elif words[0] == "info": 46 | self.run_all_blocks_fn(CodeBlock.info) 47 | else: 48 | print(f"unknown command: {words[0]}") 49 | ``` 50 | 51 | #### Command Summary: 52 | 53 | * `cmds` — Print all runnable commands in parseable JSON 54 | * `files` — Print all output files targeted by tangling 55 | * `status` — Print a human-readable summary of both commands and output files 56 | * `tangle` — Tangle all marked blocks in all `.o.md` files 57 | * `info` — Print debug info for all code blocks 58 | 59 | --- 60 | 61 | ## 🧩 Two-Word Commands 62 | 63 | These commands operate on a single named code block: 64 | 65 | ### 🔗 `@` 66 | 67 | ```python {name=handle_two_word_commands} 68 | rest = " ".join(words[1:]) 69 | 70 | if words[0] == "tangle": 71 | return self.run_block_fn(rest, CodeBlock.tangle) 72 | elif words[0] == "run": 73 | return self.run_block_fn(rest, CodeBlock.run) 74 | elif words[0] == "info": 75 | return self.run_block_fn(rest, CodeBlock.info) 76 | elif words[0] == "origin": 77 | return self.run_block_fn(rest, CodeBlock.origin) 78 | elif words[0] == "expand-str": 79 | print(self.expand(rest)) 80 | elif words[0] == "expand": 81 | print(self.expand(f"{o_sym}{rest}{c_sym}")) 82 | else: 83 | print(f"unknown command: {' '.join(words)}") 84 | ``` 85 | 86 | #### Command Summary: 87 | 88 | * `tangle ` — Tangle just the block named `` 89 | * `run ` — Run the command defined in block `` 90 | * `info ` — Print debug info for block `` 91 | * `origin ` — Print the source file that block `` came from 92 | * `expand ` — Expand the block and print its fully resolved source 93 | 94 | --- 95 | 96 | ## 🧪 Experimental Commands 97 | 98 | These multi-word commands are not finalized and are considered experimental: 99 | 100 | ### 🔗 `@` 101 | 102 | ```python {name=handle_gt_two_word_commands} 103 | if words[0] == "import": 104 | import_file(words[1], words[2]) 105 | elif words[0] == "weave": 106 | self.weave_file(words[1], words[2]) 107 | else: 108 | print(f"unknown command: {words[0]}") 109 | ``` 110 | 111 | #### Experimental Features: 112 | 113 | * `import ` — [More details here →](experimental_features.o.md) 114 | * `weave ` — [More details here →](experimental_features.o.md) 115 | 116 | --- 117 | 118 | ### 🧵 Internal Refs 119 | 120 | * [`run_block_fn`](f_run_block_fn.o.md) — Utility to dispatch methods on specific blocks 121 | * [`import_file`](experimental_features.o.md) 122 | * [`weave_file`](experimental_features.o.md) 123 | 124 | 125 | # Tests 126 | 127 | This test suite checks that the interactive command-handling logic works as expected—verifying behavior for both one-word and multi-word commands. 128 | 129 | It uses a lightweight mock `CodeBlocks` object with `reset()`, `parse()`, and `handle_cmd()` methods to keep the tests isolated and fast. 130 | 131 | --- 132 | 133 | ### 🔗 `@` 134 | 135 | ```python {name=handle_cmd_test} 136 | code_blocks.reset() 137 | cmd = "@" 138 | for i in [1]: # put this inside a loop so `break` / `continue` are valid 139 | @ 140 | 141 | @ 142 | 143 | omd_assert(@, code_blocks.parsed) 144 | omd_assert(@, code_blocks.words) 145 | ``` 146 | 147 | --- 148 | 149 | ### 🔧 Test Harness 150 | 151 | ```python {name=handle_cmd_tests menu=true} 152 | class CodeBlocks: 153 | def __init__(self): 154 | self.reset() 155 | 156 | def reset(self): 157 | self.parsed = False 158 | self.words = [] 159 | 160 | def parse(self): 161 | self.parsed = True 162 | 163 | def handle_cmd(self, words): 164 | self.words = words 165 | 166 | code_blocks = CodeBlocks() 167 | 168 | @ 172 | 173 | @ 177 | 178 | @ 182 | 183 | @ 187 | 188 | @ 189 | ``` 190 | -------------------------------------------------------------------------------- /e2e-tests.o.md: -------------------------------------------------------------------------------- 1 | Use these so that the opening and closing ref symbols don't get expanded 2 | 3 | ```bash {name=OMD_UT} 4 | ../lit/omd 5 | ``` 6 | 7 | ```bash {name=CMP_IF} 8 | if [[ "$expected" == "$actual" ]]; then 9 | printf "." 10 | else 11 | echo "Failed! Expected '$expected', Got '$actual'" 12 | fi 13 | ``` 14 | 15 | ```bash {name=CMP} 16 | expected="@" 17 | actual="@" 18 | 19 | @ 20 | ``` 21 | 22 | ```bash {name=CMP_CMD} 23 | expected="@" 24 | actual="$(@)" 25 | 26 | @ 27 | ``` 28 | 29 | ```bash {name=CMP_OMD_CMD} 30 | expected="@" 31 | actual="$(@ @)" 32 | 33 | @ 34 | ``` 35 | 36 | ```bash {name=CMP_EXPAND} 37 | expected="@" 38 | actual="$(@ expand "@")" 39 | 40 | @ 41 | ``` 42 | 43 | ```bash {name=status-expected} 44 | expected_output=$(cat <<'EOF' 45 | Available commands: 46 | (use "omd run " to execute the command) 47 | ./main.o.md: 48 | mkdir 49 | failed 50 | success 51 | four 52 | shell 53 | build_container 54 | start_container 55 | stop_container 56 | in container 57 | in_cont 58 | out container 59 | build_project 60 | run_project 61 | python_example 62 | ruby_example 63 | haskell_example 64 | racket_example 65 | perl_example 66 | javascript_example 67 | test_exec 68 | test_exec_python 69 | 70 | Output files: 71 | (use "omd tangle" to generate output files) 72 | out/Dockerfile 73 | out/main.c 74 | out/unnamed1.txt 75 | out/unnamed2.txt 76 | EOF 77 | ) 78 | ``` 79 | 80 | ```bash {name=file-test-expected} 81 | expected_output=$(cat <<'EOF' 82 | /* 83 | Copyright 2014 Adam Ard 84 | 85 | Licensed under the Apache License, Version 2.0 (the "License"); 86 | you may not use this file except in compliance with the License. 87 | You may obtain a copy of the License at 88 | 89 | http://www.apache.org/licenses/LICENSE-2.0 90 | 91 | Unless required by applicable law or agreed to in writing, software 92 | distributed under the License is distributed on an "AS IS" BASIS, 93 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 94 | See the License for the specific language governing permissions and 95 | limitations under the License. 96 | */ 97 | 98 | #include 99 | #include 100 | #include 101 | 102 | void main() 103 | { 104 | printf("Hello Organic Markdown World\n"); 105 | printf("testing_me-1\n"); 106 | 107 | printf("testing_me-2\n"); 108 | 109 | printf("testing_me-3\n"); 110 | 111 | printf("testing_me-5\n"); 112 | /* 113 | * printf("testing_me-1\n"); 114 | * 115 | * printf("testing_me-2\n"); 116 | * 117 | * printf("testing_me-3\n"); 118 | * 119 | * printf("testing_me-5\n"); 120 | */ 121 | // wonderful-> 1 <-end1 122 | // wonderful-> 1 <-end2 123 | // wonderful-> 2 <-end1 124 | // wonderful-> 2 <-end2 125 | // wonderful-> 3 <-end1 126 | // wonderful-> 3 <-end2 127 | // wonderful-> 4 <-end1 128 | // wonderful-> 4 <-end2 129 | // wonderful-> 5 <-end1 130 | // wonderful-> 5 <-end2 131 | // wonderful-> 6 <-end1 132 | // wonderful-> 6 <-end2 133 | // wonderful-> 7 <-end1 134 | // wonderful-> 7 <-end2 135 | // wonderful-> 8 <-end1 136 | // wonderful-> 8 <-end2 137 | // wonderful-> 9 <-end1 138 | // wonderful-> 9 <-end2 139 | // wonderful-> 10 <-end1 140 | // wonderful-> 10 <-end2 141 | // wonderful-> 11 <-end1 142 | // wonderful-> 11 <-end2 143 | // wonderful-> 12 <-end1 144 | // wonderful-> 12 <-end2 145 | // wonderful-> 13 <-end1 146 | // wonderful-> 13 <-end2 147 | // wonderful-> 14 <-end1 148 | // wonderful-> 14 <-end2 149 | // wonderful-> 15 <-end1 150 | // wonderful-> 15 <-end2 151 | } 152 | EOF 153 | ) 154 | ``` 155 | 156 | ```bash {name=e2e-tests dir=e2e-tests menu=true} 157 | # remove output files 158 | rm -rf out 159 | 160 | # add back the out dir 161 | mkdir out 162 | 163 | # tangle all the files 164 | @ tangle 165 | 166 | echo "Running All E2E Tests" 167 | 168 | # check that yaml values are coming through 169 | @ 170 | 171 | # check a different calling convention 172 | @ 173 | 174 | # check the yaml values do string substitution correctly 175 | @ 176 | 177 | # test multiline ref 178 | expected_output="This is 1 thing that I said. 179 | This is another: 2. 180 | And this: 3" 181 | @ 182 | 183 | expected_output="This is 11 thing that I said. 184 | This is another: 22. 185 | And this: 33" 186 | @ 187 | 188 | # test that the fields append when a name is reused 189 | expected_output="2024 190 | hello" 191 | @ 192 | 193 | # make sure that unnamed code src-blocks DON'T append 194 | @ 195 | @ 196 | 197 | expected_output="Here goes nothing testing 1 198 | Here goes nothing testing 2" 199 | @ 200 | 201 | # file test 202 | @ 203 | @ 204 | 205 | # make sure that we still traverse into subfolders 206 | @ 207 | @ 208 | @ 209 | @ 210 | 211 | # test python, ruby, haskell, racket, perl, javascript 212 | @ 213 | @ 214 | @ 215 | @ 216 | @ 217 | @ 218 | 219 | # test argument overrides 220 | @ 221 | @ 222 | 223 | expected_output="hello \"world\"" 224 | actual_output=$(@ expand "ssh-test*") 225 | actual_output="$(echo "$actual_output" | sed 's/[[:space:]]*$//')" 226 | @ 227 | 228 | @ 229 | @ 230 | 231 | echo "" 232 | echo "Finished" 233 | ``` 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Organic Markdown 2 | 3 | The most natural (and delicious!!) way to program. **Organic Markdown** takes advantage of the Markdown extensions used by [Pandoc](https://pandoc.org/MANUAL.html)—specifically YAML blocks at the top of a file and attributes for fenced code blocks—to create next-gen literate, notebook-style documents. It's strongly influenced by Emacs Org-mode and functional programming. 4 | 5 | ## Installation 6 | 7 | To install Organic Markdown, make sure you have the following dependencies: 8 | 9 | * Pandoc (>= 3.1.12) 10 | * Python 3 11 | * pypandoc 12 | 13 | Then click on `Releases` to the right of this project page. Choose the version you want (I would recommend the latest). You'll see 3 Assets: `omd`, `Source code (zip)`, `Source code (tar.gz)`. Click on `omd` to download the main script. Put it somewhere in your path. I personally put it in my user's `~/bin` directory. Make it executable: 14 | 15 | ``` 16 | chmod u+x ~/bin/omd 17 | ``` 18 | 19 | You're all set! 20 | 21 | ## Using OMD 22 | 23 | To create an Organic Markdown file, make a new file with the extension `*.o.md`. By default, `omd` reads all `.o.md` files in the current directory (and all subdirectories recursively). 24 | 25 | Let's create a simple example. Make a new directory called `test`, and inside it, create a file called `LIT.o.md`: 26 | 27 | `LIT.o.md` 28 | 29 | ``````markdown 30 | # Simple command 31 | 32 | This is an Organic Markdown test file. To create a notebook-style command, add a code block with some simple Bash code: 33 | 34 | ```bash {name=pwd menu=true} 35 | pwd 36 | ``` 37 | `````` 38 | 39 | You've just defined an executable code block. An Organic Markdown block starts with a language declaration, followed by curly-brace attributes. Here, you've named the block `pwd` and set `menu=true` to make it show up as a runnable command. 40 | 41 | Now run this in the same directory: 42 | 43 | ```bash 44 | omd status 45 | ``` 46 | 47 | You should see something like: 48 | 49 | ``` 50 | Available commands: 51 | (use "omd run " to execute the command) 52 | pwd 53 | 54 | Output files: 55 | (use "omd tangle" to generate output files) 56 | ``` 57 | 58 | To execute the command: 59 | 60 | ```bash 61 | omd run pwd 62 | ``` 63 | 64 | You can also specify the working directory for a code block: 65 | 66 | ``````markdown 67 | ```bash {name=pwd menu=true dir=/var/log} 68 | pwd 69 | ``` 70 | `````` 71 | 72 | Then run `omd run pwd` again. This time the output should be `/var/log`. 73 | 74 | ## Files and Tangling 75 | 76 | Organic Markdown also supports more traditional literate programming via **tangling**. 77 | 78 | To write a file from a code block, use the `tangle` attribute: 79 | 80 | ``````markdown 81 | # An example script file 82 | 83 | ```bash {name=script_file tangle=test.sh} 84 | #!/bin/bash 85 | 86 | echo "This is a bash script" 87 | ``` 88 | `````` 89 | 90 | Generate the file: 91 | 92 | ```bash 93 | omd tangle script_file 94 | ``` 95 | 96 | You should now see `test.sh` in your directory. It will also show up in the output of: 97 | 98 | ```bash 99 | omd status 100 | ``` 101 | 102 | You can tangle all outputs at once: 103 | 104 | ```bash 105 | omd tangle 106 | ``` 107 | 108 | To test the script, add the following to your file: 109 | 110 | ``````markdown 111 | # To run your script 112 | 113 | ```bash {name=script menu=true} 114 | bash test.sh 115 | ``` 116 | `````` 117 | 118 | Then run: 119 | 120 | ```bash 121 | omd run script 122 | ``` 123 | 124 | ## Literate References 125 | 126 | Literate programming lets you co-locate code and documentation. This keeps your docs more accurate and encourages better habits. 127 | 128 | But Organic Markdown goes further: by using **literate references** (`@`) and **tangling**, you can write small code chunks and stitch them together into clean, readable source files. 129 | 130 | ### Basic Example 131 | 132 | ``````markdown 133 | # Say Hello 134 | 135 | ```C {tangle=main.c} 136 | #include 137 | 138 | void main() 139 | { 140 | printf("Hello\n"); 141 | } 142 | ``` 143 | 144 | # Build/Run 145 | 146 | ```bash {name=build menu=true} 147 | gcc main.c 148 | ``` 149 | 150 | ```bash {name=app menu=true} 151 | ./a.out 152 | ``` 153 | `````` 154 | 155 | Now run: 156 | 157 | ```bash 158 | omd tangle && omd run build && omd run app 159 | ``` 160 | 161 | You should see: 162 | 163 | ``` 164 | Hello 165 | ``` 166 | 167 | ### Using Literate Refs for Structure 168 | 169 | ``````markdown 170 | # Say Hello (Ref Version) 171 | 172 | ```C {tangle=main.c} 173 | #include @ 174 | 175 | void main() 176 | { 177 | @ 178 | } 179 | ``` 180 | 181 | ```C {name=includes} 182 | 183 | ``` 184 | 185 | ```C {name=main} 186 | printf("Hello\n"); 187 | ``` 188 | `````` 189 | 190 | Tangle the file and inspect `main.c`. It will contain a fully assembled version with proper indentation and substitutions. 191 | 192 | Add more blocks: 193 | 194 | ``````markdown 195 | ```C {name=includes} 196 | 197 | ``` 198 | 199 | ```C {name=main} 200 | time_t t = time(NULL); 201 | struct tm *tm = localtime(&t); 202 | printf("Hello it's %s\n", ctime(&t)); 203 | ``` 204 | `````` 205 | 206 | Now re-run: 207 | 208 | ```bash 209 | omd tangle && omd run build && omd run app 210 | ``` 211 | 212 | You should see something like: 213 | 214 | ``` 215 | Hello 216 | Hello it's Mon Mar 18 19:13:51 2024 217 | ``` 218 | 219 | Multiple blocks with the same name will have their contents **appended**. 220 | 221 | ## Reference Arguments 222 | 223 | You can also **parameterize** your refs using arguments: 224 | 225 | ### `main_template.o.md` 226 | 227 | ``````markdown 228 | ```C {name=main_template} 229 | #include @ 230 | 231 | void main() 232 | { 233 | @ 234 | } 235 | ``` 236 | `````` 237 | 238 | Use it in another file: 239 | 240 | ``````markdown 241 | ```C {tangle=main.c} 242 | @ main=@)@> 243 | ``` 244 | 245 | ```C {name=hello_includes} 246 | 247 | ``` 248 | 249 | ```C {name=hello_main} 250 | printf("Hello\n"); 251 | ``` 252 | `````` 253 | 254 | ## YAML Header Constants 255 | 256 | Use the YAML header to define constants: 257 | 258 | ``````markdown 259 | --- 260 | constants: 261 | project_name: Hello-Example-Project 262 | version: 1.23 263 | --- 264 | 265 | ```C {name=hello_main} 266 | printf("@: @: Hello\n"); 267 | ``` 268 | `````` 269 | 270 | Escape `@<` if used directly in the YAML header: 271 | 272 | ```yaml 273 | constants: 274 | example: \@ 275 | ``` 276 | 277 | ## Default Ref Values 278 | 279 | Provide fall-back values in refs using `{}` syntax: 280 | 281 | ```C {name=hello_main} 282 | printf("@: @: Hello\n"); 283 | ``` 284 | 285 | ## Executing Code Blocks and Using Output 286 | 287 | Refs can also point to the **output** of executable code blocks. Just add a `*`: 288 | 289 | ``````markdown 290 | ```bash {name=arch menu=true} 291 | echo -n `uname -m` 292 | ``` 293 | 294 | ```C {name=hello_main} 295 | printf("Hello from my: @\n"); 296 | ``` 297 | `````` 298 | 299 | This inserts the runtime result of `arch` into the tangled file. 300 | 301 | ## More Resources 302 | 303 | * [The Joy of Literate Programming](https://rethinkingsoftware.substack.com/p/the-joy-of-literate-programming) 304 | * [Organic Markdown Intro](https://rethinkingsoftware.substack.com/p/organic-markdown-intro) 305 | * [DRY on Steroids](https://rethinkingsoftware.substack.com/p/dry-on-steroids-with-literate-programming) 306 | * [Literate Testing](https://rethinkingsoftware.substack.com/p/literate-testing) 307 | * [YouTube Tutorials](https://www.youtube.com/@adam-ard/videos) 308 | 309 | ## Development 310 | 311 | All code is in literate form: [Introduction](lit/intro.o.md). To work on it you need one additional dependency: 312 | 313 | * `black` (for code formatting) 314 | 315 | Clone the repository: 316 | 317 | ```bash 318 | git clone https://github.com/adam-ard/organic-markdown.git 319 | ``` 320 | 321 | then modify the files in the `lit` directory and build new versions of `omd`. Here are the commands I use to administer the repo: [Project Commands](proj_cmd.o.md). 322 | -------------------------------------------------------------------------------- /e2e-tests/main.o.md: -------------------------------------------------------------------------------- 1 | --- 2 | constants: 3 | code_dir: ~/code 4 | project_name_recurse: \@ 5 | docker_image_name: omarkdown-example 6 | docker_container_name: omarkdown-example1 7 | username: aard 8 | project_name: /home/@/code/organic-markdown/e2e-tests 9 | copyright_year: 2024 10 | multi_word: 'one two three four five' 11 | --- 12 | 13 | # This is an Organic Markdown file for the project: @ 14 | 15 | ``` {name=multiline-ref} 16 | This is @ thing that I said. 17 | This is another: @. 18 | And this: @ 19 | ``` 20 | 21 | ``` {name=multiline-test} 22 | @ 25 | ``` 26 | 27 | ## Mkdir 28 | ```bash {name="mkdir" menu="true"} 29 | mkdir -p @ 30 | ``` 31 | 32 | ## Failed bash command 33 | ```bash {name="failed" menu="true"} 34 | ls asdfasdfasdf 35 | ``` 36 | 37 | ## Successful bash command 38 | ```bash {name="success" menu="true"} 39 | ls LIT.o.md 40 | ``` 41 | 42 | ## Append 43 | ``` {name="copyright_year"} 44 | hello 45 | ``` 46 | 47 | ```{name=msg1} 48 | this is great 49 | ``` 50 | 51 | ```bash {name=four menu=true dir="."} 52 | echo @ 53 | ``` 54 | 55 | ## Copyright Notice 56 | 57 | ```C {name="copyright-c"} 58 | /* 59 | Copyright @ Adam Ard 60 | 61 | Licensed under the Apache License, Version 2.0 (the "License"); 62 | you may not use this file except in compliance with the License. 63 | You may obtain a copy of the License at 64 | 65 | http://www.apache.org/licenses/LICENSE-2.0 66 | 67 | Unless required by applicable law or agreed to in writing, software 68 | distributed under the License is distributed on an "AS IS" BASIS, 69 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 70 | See the License for the specific language governing permissions and 71 | limitations under the License. 72 | */ 73 | ``` 74 | 75 | ```Dockerfile {name="copyright-dockerfile"} 76 | # Copyright @ Adam Ard 77 | # 78 | # Licensed under the Apache License, Version 2.0 (the "License"); 79 | # you may not use this file except in compliance with the License. 80 | # You may obtain a copy of the License at 81 | # 82 | # http://www.apache.org/licenses/LICENSE-2.0 83 | # 84 | # Unless required by applicable law or agreed to in writing, software 85 | # distributed under the License is distributed on an "AS IS" BASIS, 86 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 87 | # See the License for the specific language governing permissions and 88 | # limitations under the License. 89 | ``` 90 | 91 | ## Dockerfile 92 | 93 | ```Dockerfile {tangle=out/Dockerfile} 94 | @ 95 | 96 | FROM ubuntu:22.04 97 | 98 | RUN apt-get update && \ 99 | DEBIAN_FRONTEND=noninteractive \ 100 | TZ=America/Denver \ 101 | apt install -y \ 102 | cmake \ 103 | build-essential 104 | 105 | RUN useradd -ms /bin/bash @ 106 | USER @ 107 | WORKDIR /home/@ 108 | ``` 109 | 110 | ## Docker Shell 111 | 112 | ```bash {name="shell" menu="true"} 113 | docker exec -it @ /bin/bash 114 | ``` 115 | 116 | ## Docker Build 117 | 118 | ```bash {name="build_container" menu="true" dir=@} 119 | docker build -t @ . 120 | ``` 121 | 122 | ## Start Docker Container 123 | 124 | ```bash {name="start_container" menu="true" dir="."} 125 | docker run --rm --name @ -d \ 126 | -v ${PWD}:${PWD} \ 127 | @ \ 128 | tail -f /dev/null 129 | ``` 130 | 131 | ## Stop Docker Container 132 | 133 | ```bash {name="stop_container" menu="true"} 134 | docker stop @ 135 | ``` 136 | 137 | ## Run something in the container 138 | 139 | ```bash {name="in container" menu=true} 140 | @)@> cwd=@)@> cont=@)@> 141 | ``` 142 | 143 | ^ This works the same as this (except above does line pre-post fixing): 144 | 145 | ```bash {name="in_cont" dir=@ docker=@ menu=true} 146 | hostname 147 | pwd 148 | ls -al @ 149 | ``` 150 | 151 | ## Run something outside a container 152 | 153 | ```bash {name="out container" menu="true" dir=@} 154 | hostname 155 | pwd 156 | ls -al @ 157 | ``` 158 | 159 | ## Run something with a name with spaces in it 160 | ```{name="call_spaces"} 161 | @ 162 | ``` 163 | 164 | ## Build 165 | 166 | To build this project 167 | 168 | ```bash {name="build_project" menu="true" docker=@ dir=@} 169 | gcc main.c 170 | ``` 171 | 172 | ## Run 173 | 174 | Run this project 175 | 176 | ```bash {name="run_project" menu="true" docker=@ dir=@} 177 | ./a.out 178 | ``` 179 | 180 | 181 | ## Python Code 182 | 183 | ```python {name="python_example" menu="true"} 184 | result = sum(x**2 for x in range(1, 11) if x % 2 == 0); print(f"python: The sum of squares of even numbers from 1 to 10 is: {result}") 185 | ``` 186 | 187 | ## Ruby Code 188 | 189 | ```ruby {name="ruby_example" menu="true"} 190 | result = (1..10).select(&:even?).map { |x| x**2 }.sum; puts "ruby: The sum of squares of even numbers from 1 to 10 is: #{result}" 191 | ``` 192 | 193 | ## Haskell Code 194 | 195 | ```haskell {name="haskell_example" menu="true"} 196 | main :: IO () 197 | main = do 198 | let result = sum [x^2 | x <- [1..10], even x] 199 | putStrLn $ "haskell: The sum of squares of even numbers from 1 to 10 is: " ++ show result 200 | ``` 201 | 202 | ## racket Code 203 | 204 | ```racket {name="racket_example" menu="true"} 205 | #lang racket 206 | 207 | (displayln 208 | (string-append 209 | "racket: The sum of squares of even numbers from 1 to 10 is: " 210 | (number->string 211 | (apply + 212 | (map (lambda (x) (* x x)) 213 | (filter even? (range 1 11))))))) 214 | ``` 215 | 216 | ## perl Code 217 | 218 | ```perl {name="perl_example" menu="true"} 219 | $result = 0; $result += $_ ** 2 for grep { $_ % 2 == 0 } 1..10; print "perl: The sum of squares of even numbers from 1 to 10 is: $result\n"; 220 | ``` 221 | 222 | ## javascript Code 223 | 224 | ```javascript {name="javascript_example" menu="true"} 225 | const result = [...Array(11).keys()].slice(1).filter(x => x % 2 === 0).map(x => x ** 2).reduce((a, b) => a + b, 0); console.log(`javascript: The sum of squares of even numbers from 1 to 10 is: ${result}`); 226 | ``` 227 | 228 | 229 | ## Example Functions 230 | 231 | Print x, num times 232 | ```python {name="print_x_num_times"} 233 | for i in range(@): 234 | print(@) 235 | ``` 236 | 237 | ```C {name="test_indent"} 238 | printf("testing\n"); 239 | printf("testing\n"); 240 | printf("testing\n"); 241 | ``` 242 | 243 | ```C {name="test_nesting"} 244 | printf("@-1\n"); 245 | 246 | printf("@-2\n"); 247 | 248 | printf("@-3\n"); 249 | 250 | printf("@-5\n"); 251 | ``` 252 | 253 | ```C {name="testing_nesting_inner"} 254 | testing_me 255 | ``` 256 | 257 | ```{name="msg"} 258 | begin-> 259 | ``` 260 | 261 | ```{name="msg2"} 262 | <-end1 263 | <-end2 264 | ``` 265 | 266 | ```bash {name="test_exec" menu="true" dir="."} 267 | for i in $(seq 1 @); do 268 | echo "@ $i @" 269 | done 270 | ``` 271 | 272 | ```python {name="test_exec_python" menu="true" dir="."} 273 | testing="testing" 274 | print(f"Here goes nothing {testing} 1") 275 | print(f"Here goes nothing {testing} 2") 276 | ``` 277 | 278 | ```{name="includes"} 279 | 280 | ``` 281 | 282 | ```{name="includes"} 283 | 284 | ``` 285 | 286 | ```{name="includes"} 287 | 288 | ``` 289 | 290 | ## Test Main 291 | 292 | ```C {tangle=out/main.c} 293 | @ 294 | 295 | #include @ 296 | 297 | void main() 298 | { 299 | printf("Hello Organic Markdown World\n"); 300 | @ 301 | /* 302 | * @ 303 | */ 304 | // @" num="15")@> 305 | } 306 | ``` 307 | 308 | 309 | ## Unnamed Src Blocks 310 | 311 | Unnamed blocks should not append 312 | 313 | ```bash {tangle=out/unnamed1.txt} 314 | Unnamed 1 315 | ``` 316 | 317 | ```bash {tangle=out/unnamed2.txt} 318 | Unnamed 2 319 | ``` 320 | 321 | ## argument overrides 322 | 323 | ```{name=the_msg} 324 | Be happy! 325 | ``` 326 | 327 | ```{name=my_msg} 328 | This is my message: @ 329 | ``` 330 | 331 | The following shouldn't show up in the `omd status` output because it is missing `menu=true` 332 | 333 | ```bash {name=ssh-test ssh=aard@localhost} 334 | echo 'hello "world"' 335 | ``` 336 | -------------------------------------------------------------------------------- /lit/class_codeblocks/expand.o.md: -------------------------------------------------------------------------------- 1 | # `CodeBlocks::expand` 2 | 3 | This is the **core engine** of Organic Markdown. 4 | Everything dynamic—literate references, substitutions, code output injection, templates with arguments—is powered by this elegant yet deeply recursive expansion system. 5 | 6 | Despite its brevity, this function enables: 7 | 8 | * Named reference substitution: `@` 9 | * Execution injection: `@` 10 | * Defaults: `@` 11 | * Argument passing: `@` 12 | * Nesting, recursion, and multi-line expansion 13 | 14 | It’s deceptively small—and very sensitive to change—but it's where all the magic happens. 15 | 16 | --- 17 | 18 | ### How it Works 19 | 20 | * `expand(txt)` splits input into lines, and processes each line via `expand_line()`. 21 | * `expand_line()`: 22 | 23 | * Scans the string for the next literate reference (e.g., `@`) 24 | * If found: 25 | 26 | * Expands arguments, defaults, and whether it should execute (`*@`) 27 | * Recursively expands the referenced content or command output 28 | * Splices it into the output string 29 | * If not found: 30 | 31 | * Appends the rest of the string 32 | * Returns the fully resolved, recursively substituted line. 33 | * The final output is reassembled line-by-line using `intersperse()` to preserve context and indentation. 34 | 35 | --- 36 | 37 | ### 🔗 `@` 38 | 39 | ```python {name=codeblocks__expand} 40 | def expand(self, txt, args={}): 41 | return "\n".join( 42 | [self.expand_line(x, args) for x in split_lines(txt)] 43 | ) 44 | 45 | def expand_line(self, txt, args={}): 46 | out = [] 47 | while True: 48 | match = get_match(txt) 49 | if match is None: 50 | out.append(txt) 51 | break 52 | 53 | out.append(txt[:match["start"]]) 54 | 55 | name = match["name"] 56 | new_args = parse_args(match["args"]) 57 | blk = self.get_code_block(name) 58 | 59 | # Case 1: An argument override is provided 60 | if args is not None and name in args: 61 | out.append(self.expand(args[name], args | new_args)) 62 | 63 | # Case 2: No block exists — use the default 64 | elif blk is None: 65 | out.append(self.expand(match["default"], args | new_args)) 66 | 67 | # Case 3: Execute block and insert result 68 | elif match["exec"]: 69 | out.append(self.expand(blk.run_return_results(args | new_args), args | new_args)) 70 | 71 | # Case 4: Regular substitution 72 | else: 73 | out.append(self.expand(blk.code, args | new_args)) 74 | 75 | txt = txt[match["end"]:] 76 | 77 | return intersperse(out) 78 | ``` 79 | 80 | --- 81 | 82 | # 🧪 Testing 83 | 84 | This functionality is tested using a parallel syntax that replaces `@<...@>` with `:<...:>` to prevent substitutions from interfering during the tangle phase. 85 | 86 | The test suite covers: 87 | 88 | * Basic substitution 89 | * Nested substitution 90 | * Execution injection 91 | * Argument passing 92 | * Indentation preservation 93 | * Infinite loop protection 94 | * Recursion 95 | * Defaults 96 | * Missing values 97 | 98 | --- 99 | 100 | Here is some test data, representing a full file: 101 | 102 | ```python {name=full_file} 103 | meta_block = { "constants": { 104 | "t": "MetaMap", 105 | "c": { 106 | "code_dir": { 107 | "t": "MetaInlines", 108 | "c": [ 109 | { 110 | "t": "Str", 111 | "c": "~/code" 112 | } 113 | ] 114 | }, 115 | "project_name_recurse": { 116 | "t": "MetaInlines", 117 | "c": [ 118 | { 119 | "t": "Str", 120 | "c": ":" 121 | } 122 | ] 123 | } 124 | } 125 | }} 126 | 127 | code_block_append1 = [["", 128 | [], 129 | [["name", "append"], 130 | ]], 131 | "1"] 132 | 133 | code_block_append2 = [["", 134 | [], 135 | [["name", "append"], 136 | ]], 137 | "2"] 138 | 139 | code_block_append3 = [["", 140 | [], 141 | [["name", "append"], 142 | ]], 143 | "3"] 144 | 145 | code_block_append4 = [["", 146 | [], 147 | [["name", "append"], 148 | ]], 149 | "4"] 150 | 151 | code_block_append1_no_name = [["", [], []], "no_name_1"] 152 | code_block_append2_no_name = [["", [], []], "no_name_2"] 153 | code_block_append3_no_name = [["", [], []], "no_name_3"] 154 | code_block_append4_no_name = [["", [], []], "no_name_4"] 155 | 156 | code_block_3_lines = [["", 157 | [], 158 | [["name", "three_lines"], 159 | ]], 160 | "1\n2\n3"] 161 | 162 | code_block_b = [["", 163 | [], 164 | [["name", "b"], 165 | ]], 166 | "1\n2"] 167 | 168 | code_block_d = [["", 169 | [], 170 | [["name", "d"], 171 | ]], 172 | "3\n4"] 173 | 174 | code_block_1 = [["", 175 | [], 176 | [["name", "one"], 177 | ]], 178 | "[This is some text]"] 179 | 180 | code_block_2 = [["", 181 | [], 182 | [["name", "two"], 183 | ]], 184 | "[This is the text from block one::, wasn't that nice?]"] 185 | 186 | code_block_2_1 = [["", 187 | [], 188 | [["name", "two_1"], 189 | ]], 190 | "[This is the text from block one::, wasn't that nice?]"] 191 | 192 | code_block_2_sentences = [["", 193 | [], 194 | [["name", "two_sentences"], 195 | ]], 196 | "This is sentence 1 - :\nThis is sentence 2 - :"] 197 | 198 | code_block_3 = [["", 199 | [], 200 | [["name", "three"], 201 | ]], 202 | "[This is the text from block two::, can you believe it?]"] 203 | 204 | code_block_3_1 = [["", 205 | [], 206 | [["name", "msg"], 207 | ]], 208 | "this is great"] 209 | 210 | code_block_4 = [["", 211 | ["bash"], 212 | [["name", "four"], 213 | ["menu", "true"], 214 | ["dir", "."], 215 | ]], 216 | 'echo :'] 217 | 218 | code_block_5 = [["", 219 | ["python"], 220 | [["name", "five"], 221 | ["menu", "true"], 222 | ["dir", "."], 223 | ]], 224 | 'print("I am python!! :")'] 225 | 226 | 227 | indent_2 = [["", 228 | [], 229 | [["name", "indent_2"], 230 | ]], 231 | f"""one 232 | two 233 | three 234 | four"""] 235 | 236 | # note that I use the {""} syntax so when I run whitespace-cleanup it doesn't mess with the spaces 237 | indent_3 = [["", 238 | [], 239 | [["name", "indent_3"], 240 | ]], 241 | f"""one 242 | {""} 243 | 244 | two 245 | three 246 | four"""] 247 | 248 | indent_4 = [["", 249 | [], 250 | [["name", "indent_4"], 251 | ]], 252 | """indent_block { 253 | : 254 | }"""] 255 | 256 | indent_5 = [["", 257 | [], 258 | [["name", "indent_5"], 259 | ]], 260 | f"""one 261 | {" "} 262 | 263 | two 264 | three 265 | four"""] 266 | 267 | indent_6 = [["", 268 | [], 269 | [["name", "indent_6"], 270 | ]], 271 | """indent_block { 272 | // : 273 | }"""] 274 | 275 | 276 | 277 | full_file = {"blocks": [{"t": "", 278 | "c": meta_block 279 | }, 280 | {"t": "CodeBlock", 281 | "c": code_block_1 282 | }, 283 | {"t": "CodeBlock", 284 | "c": code_block_3_lines 285 | }, 286 | {"t": "CodeBlock", 287 | "c": code_block_b 288 | }, 289 | {"t": "CodeBlock", 290 | "c": code_block_append1 291 | }, 292 | {"t": "CodeBlock", 293 | "c": code_block_2_sentences 294 | }, 295 | {"t": "CodeBlock", 296 | "c": code_block_append2 297 | }, 298 | {"t": "CodeBlock", 299 | "c": code_block_append3 300 | }, 301 | {"t": "CodeBlock", 302 | "c": code_block_append4 303 | }, 304 | {"t": "CodeBlock", 305 | "c": code_block_append1_no_name 306 | }, 307 | {"t": "CodeBlock", 308 | "c": code_block_append2_no_name 309 | }, 310 | {"t": "CodeBlock", 311 | "c": code_block_append3_no_name 312 | }, 313 | {"t": "CodeBlock", 314 | "c": code_block_append4_no_name 315 | }, 316 | {"t": "CodeBlock", 317 | "c": code_block_d 318 | }, 319 | {"t": "CodeBlock", 320 | "c": code_block_2 321 | }, 322 | {"t": "CodeBlock", 323 | "c": code_block_2_1 324 | }, 325 | {"t": "CodeBlock", 326 | "c": indent_2 327 | }, 328 | {"t": "CodeBlock", 329 | "c": indent_3 330 | }, 331 | {"t": "CodeBlock", 332 | "c": indent_4 333 | }, 334 | {"t": "CodeBlock", 335 | "c": indent_5 336 | }, 337 | {"t": "CodeBlock", 338 | "c": indent_6 339 | }, 340 | {"t": "CodeBlock", 341 | "c": code_block_3 342 | }, 343 | {"t": "CodeBlock", 344 | "c": code_block_3_1 345 | }, 346 | {"t": "CodeBlock", 347 | "c": code_block_5 348 | }, 349 | {"t": "CodeBlock", 350 | "c": code_block_4 351 | },], 352 | "pandoc-api-version": [1, 20], 353 | "meta": meta_block 354 | } 355 | ``` 356 | 357 | ### 🧪 Expand Tests 358 | 359 | ```python {name=expand_tests menu=true} 360 | @ 361 | 362 | o_sym = ":<" 363 | c_sym = ":>" 364 | 365 | @ 366 | @ 367 | 368 | @ 369 | @ 370 | 371 | class CodeBlocks: 372 | def __init__(self): 373 | self.code_blocks = [] 374 | @ 375 | @ 376 | @ 377 | 378 | blks = CodeBlocks() 379 | blks.parse_json(full_file, "test_file.omd") 380 | 381 | def test(txt, expected_expanded): 382 | expanded = blks.expand(txt) 383 | omd_assert(expected_expanded, expanded) 384 | 385 | test(':', "[This is the text from block two:asdf, can you believe it?]") 386 | test(':', "[This is the text from block one:qwerty, wasn't that nice?]") 387 | test(':', "this is great") 388 | test(':', "I am python!! asdf") 389 | test(':"):>', """This is sentence 1 - 1 390 | This is sentence 1 - 2 391 | This is sentence 1 - 3 392 | This is sentence 2 - 1 393 | This is sentence 2 - 2 394 | This is sentence 2 - 3""") 395 | test(':', "here is a msg") 396 | test(':', "[This is the text from block one:qwerty, wasn't that nice?]") 397 | test(':', f"""indent_block {{ 398 | one 399 | {""} 400 | {""} 401 | two 402 | three 403 | four 404 | }}""") 405 | test(':', f"""indent_block {{ 406 | // one 407 | // {" "} 408 | // {""} 409 | // two 410 | // three 411 | // four 412 | }}""") 413 | 414 | 415 | test('--->::<-----', """--->one[This is some text]<----- 416 | --->two[This is some text]<----- 417 | --->three[This is some text]<----- 418 | --->four[This is some text]<-----""") 419 | 420 | # this will cause an infinite loop if we do this wrong 421 | test('a:c:e', "a1c3e\na2c4e") 422 | test("a:b", "a[This is the text from block two:2, can you believe it?]b") 423 | test(':', "1\n2\n3\n4") 424 | test(':', "~/code") 425 | test(':', "") 426 | test(':', "") 427 | 428 | test("[This is the text from block two::, can you believe it?]", "[This is the text from block two:[This is the text from block one:[This is some text], wasn't that nice?], can you believe it?]") 429 | @ 430 | ``` 431 | --------------------------------------------------------------------------------