Initial commit.
This commit is contained in:
commit
4e4ed86483
8 changed files with 392 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.vscode/
|
||||
**/__pycache__/
|
||||
73
README.md
Normal file
73
README.md
Normal 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
|
||||
```
|
||||
5
example/include/factorial.h
Normal file
5
example/include/factorial.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// BEGIN CODE SNIPPET header_factorial
|
||||
#pragma once
|
||||
|
||||
int factorial(int n);
|
||||
// END CODE SNIPPET
|
||||
24
example/src/example_code.cpp
Normal file
24
example/src/example_code.cpp
Normal 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
|
||||
8
example/src/factorial.cpp
Normal file
8
example/src/factorial.cpp
Normal 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
|
||||
189
markdown_snippet_injector.py
Normal file
189
markdown_snippet_injector.py
Normal 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
53
processed_markdown.md
Normal 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
38
source_markdown.md
Normal 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"/>
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue