NeuroKLEA / scripts /nml-docs-parser /jupyterbook2singlemd.py
sanjayankur31's picture
chore: use a better name
72a8d06 verified
Raw History Blame Contribute Delete
8.21 kB
#!/usr/bin/env python3
"""
Generate a single page markdown from Jupyter book sources
File: data/scripts/jupyterbook2singlemd.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import json
import logging
import re
import sys
from itertools import takewhile
from pathlib import Path
import colorlog
logging.basicConfig(level=logging.NOTSET)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatter = colorlog.ColoredFormatter(
"%(log_color)s%(name)s (%(levelname)s): %(message)s"
)
handler = colorlog.StreamHandler()
handler.setLevel(logging.INFO)
handler.setFormatter(formatter)
logger.addHandler(handler)
def runner(source: str):
"""Main runner
:param source: TODO
:returns: TODO
"""
toc = f"{source}/_toc.yml"
filelist = []
url_base = "https://docs.neuroml.org"
url_map = {"DEFAULT": {"url": url_base}}
with open(toc, "r") as toc_f:
for line in toc_f:
if "file:" in line or "root:" in line:
if "file:" in line:
source_file = Path(line.split("file:")[1].strip())
else:
source_file = Path(line.split("root:")[1].strip())
if source_file.suffix == "":
source_file_full = f"{source_file}.md"
else:
source_file_full = str(source_file)
# ignore notebooks
if source_file_full.endswith("ipynb"):
continue
print(f"Found: {source_file_full}")
filelist.append(source_file_full)
logger.debug(f"{filelist = }")
text = ""
schema_text = ""
# ignore lines starting with these
start_ignores = (
"----",
"language: ",
"lines: ",
":class: ",
":align: ",
":alt: ",
":scale: ",
":gutter: ",
":columns: ",
":widths: ",
":width: ",
":delim: ",
":all:",
"%",
)
# matches replace below
admons = ["admonition", "note", "warning", "tip", "important"]
# note that the order in which these are listed is important, since the
# regular expression substitutions are done sequentially in multiple passes
refs = {}
replacements = {
r"{ref}`(.+?)>`": r"\1>",
r"{ref}`(.+?)`": r"\1",
r"{doc}`(.+?)>`": r"\1>",
r"{eq}`(.+?)>`": r" (see equation \1)",
r"{cite}`(.+?)`": r"[citation: \1]",
r"{superscript}`(.+?)`": r"^\1",
# table inside tabs
r"`{4}{tab-item} (.+)\n`{3}{csv-table}\n": r"Table of \1 (separator='$')\n```\nName $ description $ reference\n",
# simple tabs
r"`{4}{tab-item} (.+)\n": r"\1\n",
# figures
r"{(image|figure)} (.+)": r"\nFigure: \2",
# admons
r"{(admonition|tip|warning|note|important)}": r"\nNOTE: ",
# other bracketed bits
r"{(code|code-block|download|grid-item-card|grid|tab-set|csv-table)}": r"",
# misc
r"(schema:|units:|<i>|</i>|&emsp;|`{5}|`{4})": r"",
r"(`{4})": r"\n",
}
for srcfile in filelist:
srcfilepath = Path(f"{source}/{srcfile}")
print(f"Processing {srcfilepath}")
adding_text_to = ""
with open(srcfilepath, "r") as srcfile_f:
in_block: list[str] = []
section_ref = ""
for line in srcfile_f:
# handle code includes
if line.startswith(start_ignores):
logger.warning(f"Ignoring line: {line = }")
continue
# logger.debug(f"Processing line: {line = }")
# section heading
if line.startswith("(") and line.strip().endswith(")="):
section_ref = f"<{line[1:-3]}>"
continue
# header
if not in_block and line.startswith("#"):
header = line.replace("#", "", count=-1)
url_map[header.strip()] = {
"url": f"{url_base}/{srcfile.replace('.md', '.html')}"
}
# section ref preceds headers, processed in the next line after section heading is processed
if len(section_ref) > 0:
refs[section_ref] = (
"(see section: " + line.replace("#", "").strip() + ")"
)
section_ref = ""
adding_text_to += "\n" + line
continue
if line.startswith("`"):
leading_backticks = "".join(
takewhile(lambda char: char == "`", line)
)
# single backticks arent blocks
if len(leading_backticks) < 2:
adding_text_to += line
continue
if len(in_block) and in_block[-1] == leading_backticks:
logger.debug(f"Leaving block: {leading_backticks}")
adding_text_to += f"{line.rstrip()}\n"
in_block.pop()
continue
in_block.append(leading_backticks)
logger.debug(f"Entered block {leading_backticks}")
if "{literalinclude}" in line:
file_to_include = line.split("{literalinclude}")[1].strip()
with open(
f"{srcfilepath.parent}/{file_to_include}", "r"
) as incfile_f:
included_cont = incfile_f.read()
adding_text_to += f"\n\n```\n\n{included_cont}\n\n"
continue
if "{bibliography}" in line:
file_to_include = (
line.split("{bibliography}")[1]
.strip()
.replace(".bib", ".md")
)
logger.info(f"Including bibliography file {file_to_include = }")
with open(
f"{srcfilepath.parent}/{file_to_include}", "r"
) as incfile_f:
included_cont = incfile_f.read()
adding_text_to += f"\n\n{included_cont}\n\n"
continue
isadmon = False
for m in admons:
if f"{{{m}}}" in line:
isadmon = True
admon_text = line.split(f"{{{m}}}")
adding_text_to += (
"```\n"
+ m.upper()
+ "\n"
+ admon_text[1].strip()
+ "\n\n"
)
break
if not isadmon:
adding_text_to += line
else:
adding_text_to += line.rstrip() + "\n"
if "Schemas/" in str(srcfilepath):
schema_text += adding_text_to
else:
text += adding_text_to
for pat, rep in replacements.items():
text = re.sub(pat, rep, text, count=0, flags=re.M)
for pat, rep in refs.items():
text = re.sub(pat, rep, text, count=0)
for pat, rep in replacements.items():
schema_text = re.sub(pat, rep, schema_text, count=0, flags=re.M)
for pat, rep in refs.items():
schema_text = re.sub(pat, rep, schema_text, count=0)
with open("single-page-markdown.md", "w") as out:
print(text, file=out)
with open("single-page-markdown-schema.md", "w") as out:
print(schema_text, file=out)
with open("url-map.json", "w") as f:
json.dump(url_map, f)
# print(refs)
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("Only one argument permitted: location of source folder")
runner(sys.argv[1])