Initial commit.

This commit is contained in:
Jérôme 2026-02-04 14:09:41 +01:00
commit 4e4ed86483
8 changed files with 392 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.vscode/
**/__pycache__/

73
README.md Normal file
View file

@ -0,0 +1,73 @@
# Code snippet markdown insertion tool
This tool allows the definition of tags within any code base to be referred to in a markdown documentation file.
The specific XML tags present in the markdown file are replaced by the corresponding code snippet, pulled directly from the linked source code file.
This allows the documentation to evolve automatically when the source files are modified, thus reducing the maintenance effort of such documentation, especially in cases where minor changes in the code do not necesitate updating the documentation, but that the snippets must be kept up-to-date with the actual code.
## Usage
```bash
python markdown_snippet_injector.py source_markdown.md processed_markdown.md
```
## Example
Here are links to examples of an [unprocessed](source_markdown.md) and the corresponding [processed](processed_markdown.md) markdown files.
## Markdown XML tags
The source markdown file has specific XML tags `<include_snippet/>` embedded that will be replaced by the actual code contained within the corresponding tags in the related source files.
Two parameters must be specified :
- name : unique name of the snippet
- file : file path in which to find the snippet
```xml
<include_snippet name="snippet_name" file="source_code_file.cpp"/>
```
Here is a simple example of a markdown file containing a tag :
```markdown
## The includes
The includes are important to tell the compiler what to expect later. In the example file, the includes are :
```cpp
<include_snippet name="includes" file="example_code.cpp"/>
```
```
## Code file tags
In the code files, the tags must be under the following form :
```
BEGIN CODE SNIPPET snippet_name
Some code that will be included in the snippet
END CODE SNIPPET
```
Note that the entire line where BEGIN CODE SNIPPET and END CODE SNIPPET is removed from the output markdown file.
It is recommended to put the tags in separate lines around the snippet you want to capture.
Depending on the language of the source file, the appropriate comment format must be used, so that the tag's presence does not change the behavior of the program.
Example in python :
```python
# BEGIN CODE SNIPPET snippet_name
v1 = np.array([1.0, 2.0, 3.0])
# END CODE SNIPPET
```
Example in C++ :
```cpp
// BEGIN CODE SNIPPET snippet_name
const auto v1 = {1.0, 2.0, 3.0};
// END CODE SNIPPET
```

View file

@ -0,0 +1,5 @@
// BEGIN CODE SNIPPET header_factorial
#pragma once
int factorial(int n);
// END CODE SNIPPET

View file

@ -0,0 +1,24 @@
// BEGIN CODE SNIPPET includes
#include <iostream>
#include <factorial.h>
// END CODE SNIPPET
int some_function(int x) {
int y = x*x;
// BEGIN CODE SNIPPET some_function
int z = y*y;
return z;
// END CODE SNIPPET
}
// BEGIN CODE SNIPPET main_function
int main() {
// Call the factorial function
std::cout << factorial(5) << std::endl;
// Call some_function
std::cout << some_function(3) << std::endl;
return 0;
}
// END CODE SNIPPET

View file

@ -0,0 +1,8 @@
// BEGIN CODE SNIPPET factorial_function
int factorial(int n) {
if (n <= 1) {
return 1;
}
return factorial(n-1) * n;
}
// END CODE SNIPPET

View file

@ -0,0 +1,189 @@
import argparse
import re
import xml.etree.ElementTree as ET
def extract_code_snippets(file_path: str) -> dict:
"""
Extracts code snippets delimited by "BEGIN CODE SNIPPET XYZ" and "END CODE SNIPPET" from the given file, with XYZ being some user-defined name.
All snippets are extracted and returned as a dictionary with the snippet name as the key, and the snippet contents as the value.
:param file_path: File path in which the tag should be found.
"""
snippets = dict()
with open(file_path) as f_in:
lines = f_in.readlines()
snippet_name = None
snippet_start_line = None
for i, line in enumerate(lines):
if "BEGIN CODE SNIPPET" in line:
snippet_name = line.split("BEGIN CODE SNIPPET")[1].strip()
snippet_start_line = i + 1
if "END CODE SNIPPET" in line:
snippets[snippet_name] = "".join(lines[snippet_start_line:i])
return snippets
def decode_xml_snippet_tag(line):
"""
Extracts and decodes the XML snippet tag under the following form :
```
<include_snippet name="snippet_name" file="path/to/file.cpp"/>
```
:param line: Line containing the XML tag.
"""
# Extract the XML tag from the line
the_match = re.match("(<include_snippet+.+\\/>)", line)
if the_match:
xml_string = the_match.group(1)
else:
return None
# Parse the XML string
root = ET.fromstring(f'<root>{xml_string}</root>') # Wrap in a root tag if needed
# Iterate over all include_snippet tags
for elem in root.findall('include_snippet'):
name = elem.get('name')
file = elem.get('file')
return {"name": name, "file": file}
def extract_snippet_infos(lines) -> dict:
"""
Extracts the names of the snippets required from the source markdown file.
Returns a dictionary of lists :
- file_name_1
- snippet_name_1
- snippet_name_2
- ...
- file_name_2
- snippet_name_1
- snippet_name_2
- ...
- ...
"""
snippet_infos = dict()
snippet_infos_list = []
for line in lines:
if "<include_snippet" in line.strip():
snippet_info = decode_xml_snippet_tag(line)
snippet_infos_list.append(snippet_info)
# Group the snippets by file
for snippet_info in snippet_infos_list:
if snippet_info["file"] not in snippet_infos.keys():
snippet_infos[snippet_info["file"]] = []
snippet_infos[snippet_info["file"]].append(snippet_info["name"])
return snippet_infos
def insert_snippets_in_markdown(source_lines: list, snippets: dict):
"""
Inserts the required snippets into the markdown file.
Returns the lines of the output markdown file.
"""
processed_lines = []
for line in source_lines:
if "<include_snippet" in line.strip():
# Parse and decode the XML snippet tag
snippet_info = decode_xml_snippet_tag(line)
# Append the snippet contents to the output lines
processed_lines.extend(snippets[snippet_info["file"]][snippet_info["name"]])
else:
# Append the source line verbatim
processed_lines.append(line)
return processed_lines
def print_snippet_information(snippet_infos: dict):
"""
Prints the snippet information parsed from the source markdown.
:param snippet_infos: Dict of file name and lists of snippet names.
:type snippet_infos: dict
"""
print("Required code snippets")
print("----------------------")
for file in snippet_infos:
print(f"\"{file}\"")
for name in snippet_infos[file]:
print(f" \"{name}\"")
def print_snippet_summary(snippets: dict):
"""
Prints the snippet information parsed from the code files.
:param snippets: Dict of file name and dict of snippet names and contents.
:type snippets: dict
"""
print("\nFound code snippets")
print("-------------------")
for file in snippets:
print(f"\"{file}\"")
for name in snippets[file]:
print(f" \"{name}\"")
def process_markdown_file(args):
"""
Processes the markdown file by finding and replacing all the `<include_snippet/>` tags under the following form:
```
<include_snippet name="snippet_name" file="path/to/file.cpp"/>
```
Beware that the `<include_snippet/>` tag is case-sensitive ! It must be all lowercase to be correctly detected.
:param args: Parsed program arguments.
"""
with open(args.source) as f_in:
with open(args.destination, "w+") as f_out:
# Read source markdown file
source_lines = f_in.readlines()
# Extract required code snippets from markdown file
snippet_infos = extract_snippet_infos(source_lines)
print_snippet_information(snippet_infos)
# Extract actual snippets
snippets = dict()
for file_path in snippet_infos.keys():
snippets[file_path] = extract_code_snippets(file_path)
print_snippet_summary(snippets)
# Replace the snippet calls by the actual content
output_lines = insert_snippets_in_markdown(source_lines, snippets)
# Write the processed file
f_out.writelines(output_lines)
def parse_args():
"""
Parses the arguments of the program.
"""
# Create the argument parser
parser = argparse.ArgumentParser(description='Markdown code snippet injector.')
# Add the positional arguments
parser.add_argument('source', type=str, help='Source path (mandatory)')
parser.add_argument('destination', type=str, help='Destination path (mandatory)')
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
process_markdown_file(args)

53
processed_markdown.md Normal file
View file

@ -0,0 +1,53 @@
# Example markdown
Compare the processed version of this file with its unprocessed form.
## The includes
The includes are important to tell the compiler what to expect later. In the example file, the includes are :
```cpp
#include <iostream>
#include <factorial.h>
```
## The factorial function
The factorial function is implemented recursively. It is defined in another file called `factorial.cpp` :
```cpp
int factorial(int n) {
if (n <= 1) {
return 1;
}
return factorial(n-1) * n;
}
```
The corresponding header file is :
```cpp
#pragma once
int factorial(int n);
```
## Some other function
Some other function is implemented in the `example_code.cpp` file.
While the snippet is defined in the C++ file, we can decide to not use it in the documentation.
## Main function
In order to call all the functions defined above, the main function is implemented as such :
```cpp
int main() {
// Call the factorial function
std::cout << factorial(5) << std::endl;
// Call some_function
std::cout << some_function(3) << std::endl;
return 0;
}
```

38
source_markdown.md Normal file
View file

@ -0,0 +1,38 @@
# Example markdown
Compare the processed version of this file with its unprocessed form.
## The includes
The includes are important to tell the compiler what to expect later. In the example file, the includes are :
```cpp
<include_snippet name="includes" file="example/src/example_code.cpp"/>
```
## The factorial function
The factorial function is implemented recursively. It is defined in another file called `factorial.cpp` :
```cpp
<include_snippet name="factorial_function" file="example/src/factorial.cpp"/>
```
The corresponding header file is :
```cpp
<include_snippet name="header_factorial" file="example/include/factorial.h"/>
```
## Some other function
Some other function is implemented in the `example_code.cpp` file.
While the snippet is defined in the C++ file, we can decide to not use it in the documentation.
## Main function
In order to call all the functions defined above, the main function is implemented as such :
```cpp
<include_snippet name="main_function" file="example/src/example_code.cpp"/>
```