#|hide
#default_exp export2html
#default_cls_lvl 3
from nbdev.showdoc import show_doc
#|export
from nbdev.imports import *
from nbdev.sync import *
from nbdev.export import *
from nbdev.export import _mk_flag_re
from nbdev.showdoc import *
from nbdev.template import *
from fastcore.foundation import *
from fastcore.script import *
from html.parser import HTMLParser
from nbconvert.preprocessors import ExecutePreprocessor, Preprocessor
from nbconvert import HTMLExporter,MarkdownExporter
import traitlets
import nbformat
#|export
class HTMLParseAttrs(HTMLParser):
"Simple HTML parser which stores any attributes in `attrs` dict"
def handle_starttag(self, tag, attrs): self.tag,self.attrs = tag,dict(attrs)
def attrs2str(self):
"Attrs as string"
return ' '.join([f'{k}="{v}"' for k,v in self.attrs.items()])
def show(self):
"Tag with updated attrs"
return f'<{self.tag} {self.attrs2str()} />'
def __call__(self, s):
"Parse `s` and store attrs"
self.feed(s)
return self.attrs
h = HTMLParseAttrs()
t = h('')
test_eq(t['width'], '700')
test_eq(t['src' ], 'src')
t['width'] = '600'
test_eq(h.show(), '')
#|export
def remove_widget_state(cell):
"Remove widgets in the output of `cells`"
if cell['cell_type'] == 'code' and 'outputs' in cell:
cell['outputs'] = [l for l in cell['outputs']
if not ('data' in l and 'application/vnd.jupyter.widget-view+json' in l.data)]
return cell
#|export
# Note: `_re_show_doc` will catch show_doc even if it's commented out etc
_re_show_doc = re.compile(r"""
# Catches any show_doc and get the first argument in group 1
^\s*show_doc # line can start with any amount of whitespace followed by show_doc
\s*\(\s* # Any number of whitespace, opening (, any number of whitespace
([^,\)\s]*) # Catching group for any character but a comma, a closing ) or a whitespace
[,\)\s] # A comma, a closing ) or a whitespace
""", re.MULTILINE | re.VERBOSE)
_re_hide_input = [
_mk_flag_re('export', (0,1), "Cell that has `#export"),
_mk_flag_re('(hide_input|hide-input)', 0, "Cell that has `#hide_input` or `#hide-input`")]
_re_hide_output = _mk_flag_re('(hide_output|hide-output)', 0, "Cell that has `#hide_output` or `#hide-output`")
#|export
def upd_metadata(cell, key, value=True):
"Sets `key` to `value` on the `metadata` of `cell` without replacing metadata"
cell.setdefault('metadata',{})[key] = value
#|export
def hide_cells(cell):
"Hide inputs of `cell` that need to be hidden"
if check_re_multi(cell, [_re_show_doc, *_re_hide_input]): upd_metadata(cell, 'hide_input')
elif check_re(cell, _re_hide_output): upd_metadata(cell, 'hide_output')
return cell
for source in ['show_doc(read_nb)', '# export\nfrom local.core import *', '# hide_input\n2+2',
'line1\n show_doc (read_nb) \nline3', '# export with.mod\nfrom local.core import *']:
cell = {'cell_type': 'code', 'source': source}
cell1 = hide_cells(cell.copy())
assert 'metadata' in cell1
assert 'hide_input' in cell1['metadata']
assert cell1['metadata']['hide_input']
flag = '# exports'
cell = {'cell_type': 'code', 'source': f'{flag}\nfrom local.core2 import *'}
test_eq(hide_cells(cell.copy()), cell)
for source in ['# hide-output\nfrom local.core import *', '# hide_output\n2+2']:
cell = {'cell_type': 'code', 'source': source}
cell1 = hide_cells(cell.copy())
assert 'metadata' in cell1
assert 'hide_output' in cell1['metadata']
assert cell1['metadata']['hide_output']
cell = {'cell_type': 'code', 'source': '# hide-outputs\nfrom local.core import *'}
test_eq(hide_cells(cell.copy()), cell)
#|export
def clean_exports(cell):
"Remove all flags from code `cell`s"
if cell['cell_type'] == 'code': cell['source'] = split_flags_and_code(cell, str)[1]
return cell
flag = '# exports'
cell = {'cell_type': 'code', 'source': f'{flag}\nfrom local.core import *'}
test_eq(clean_exports(cell.copy()), {'cell_type': 'code', 'source': 'from local.core import *'})
cell['cell_type'] = 'markdown'
test_eq(clean_exports(cell.copy()), cell)
cell = {'cell_type': 'code', 'source': f'{flag} core\nfrom local.core import *'}
test_eq(clean_exports(cell.copy()), {'cell_type': 'code', 'source': 'from local.core import *'})
cell = {'cell_type': 'code', 'source': f'# comment \n# exports\nprint("something")'}
test_eq(clean_exports(cell.copy()), {'cell_type': 'code', 'source': '# exports\nprint("something")'})
#|export
def treat_backticks(cell):
"Add links to backticks words in `cell`"
if cell['cell_type'] == 'markdown': cell['source'] = add_doc_links(cell['source'])
return cell
cell = {'cell_type': 'markdown', 'source': 'This is a `DocsTestClass`'}
# test_eq(treat_backticks(cell), {'cell_type': 'markdown',
# 'source': 'This is a [`DocsTestClass`](/export.html#DocsTestClass)'})
#|export
_re_nb_link = re.compile(r"""
# Catches any link to a local notebook and keeps the title in group 1, the link without .ipynb in group 2
\[ # Opening [
([^\]]*) # Catching group for any character except ]
\]\( # Closing ], opening (
([^http] # Catching group that must not begin by html (local notebook)
[^\)]*) # and containing anything but )
.ipynb\) # .ipynb and closing )
""", re.VERBOSE)
#|export
_re_block_notes = re.compile(r"""
# Catches any pattern > Title: content with title in group 1 and content in group 2
^\s*>\s* # > followed by any number of whitespace
([^:]*) # Catching group for any character but :
:\s* # : then any number of whitespace
([^\n]*) # Catching group for anything but a new line character
(?:\n|$) # Non-catching group for either a new line or the end of the text
""", re.VERBOSE | re.MULTILINE)
#|export
def _to_html(text):
return text.replace("'", "’")
#|export
def add_jekyll_notes(cell):
"Convert block quotes to jekyll notes in `cell`"
styles = get_config().get('jekyll_styles', 'note,warning,tip,important').split(',')
def _inner(m):
title,text = m.groups()
if title.lower() not in styles: return f"> {title}:{text}"
return '{% include '+title.lower()+".html content=\'"+_to_html(text)+"\' %}"
if cell['cell_type'] == 'markdown':
cell['source'] = _re_block_notes.sub(_inner, cell['source'])
return cell
#|hide
for w in ['Warning', 'Note', 'Important', 'Tip', 'Bla']:
cell = {'cell_type': 'markdown', 'source': f"> {w}: This is my final {w.lower()}!"}
res = '{% include '+w.lower()+'.html content=\'This is my final '+w.lower()+'!\' %}'
if w != 'Bla': test_eq(add_jekyll_notes(cell), {'cell_type': 'markdown', 'source': res})
else: test_eq(add_jekyll_notes(cell), cell)
#|hide
cell = {'cell_type': 'markdown', 'source': f"> This is a link, don't break me! https://my.link.com"}
test_eq(add_jekyll_notes(cell.copy()), cell)
#|export
_re_image = re.compile(r"""
# Catches any image file used, either with `![alt](image_file)` or ``
^(!\[ # Beginning of line (since re.MULTILINE is passed) followed by ![ in a catching group
[^\]]* # Anything but ]
\]\() # Closing ] and opening (, end of the first catching group
[ \t]* # Whitespace before the image path
([^\) \t]*) # Catching block with any character that is not ) or whitespace
(\)| |\t) # Catching group with closing ) or whitespace
| # OR
^(]*>) # Catching group with
""", re.MULTILINE | re.VERBOSE)
m=_re_image.search('![Alt](images/logo.png)')
test_eq(m.groups(), ('![Alt](', 'images/logo.png', ')', None))
# using ) or whitespace to close the group means we don't need a special case for captions
m=_re_image.search('![Alt](images/logo.png "caption (something)")')
test_eq(m.groups(), ('![Alt](', 'images/logo.png', '', None))
#|export
def _img2jkl(d, h, jekyll=True):
if d.get("src","").startswith("http"): jekyll=False
if jekyll:
if 'width' in d: d['max-width'] = d.get('width')
else:
if 'width' in d: d['style'] = f'max-width: {d.get("width")}px'
d.pop('align','')
return ''
if 'src' in d: d['file'] = d.pop('src')
return '{% include image.html ' + h.attrs2str() + ' %}'
#|export
def _is_real_image(src):
return not (src.startswith('http://') or src.startswith('https://') or src.startswith('data:image/') or src.startswith('attachment:'))
#|export
def copy_images(cell, fname, dest, jekyll=True):
"Copy images referenced in `cell` from `fname` parent folder to `dest` folder"
def _rep_src(m):
grps = m.groups()
if grps[3] is not None:
h = HTMLParseAttrs()
dic = h(grps[3])
src = dic['src']
else: src = grps[1]
if _is_real_image(src):
os.makedirs((Path(dest)/src).parent, exist_ok=True)
shutil.copy(Path(fname).parent/src, Path(dest)/src)
src = get_config().doc_baseurl + src
if grps[3] is not None:
dic['src'] = src
return _img2jkl(dic, h, jekyll=jekyll)
else: return f"{grps[0]}{src}{grps[2]}"
if cell['cell_type'] == 'markdown': cell['source'] = _re_image.sub(_rep_src, cell['source'])
return cell
dest_img = get_config().path("doc_path")/'images'/'logo.png'
cell = {'cell_type': 'markdown', 'source':'Text\n![Alt](images/logo.png)'}
try:
copy_images(cell, Path('01_export.ipynb'), get_config().path("doc_path"))
test_eq(cell["source"], 'Text\n![Alt](/images/logo.png)')
#Image has been copied
assert dest_img.exists()
cell = {'cell_type': 'markdown', 'source':'Text\n![Alt](images/logo.png "caption (something)")'}
copy_images(cell, Path('01_export.ipynb'), get_config().path("doc_path"))
test_eq(cell["source"], 'Text\n![Alt](/images/logo.png "caption (something)")')
finally: dest_img.unlink()
#|hide
cell = {'cell_type': 'markdown', 'source':'Text\n![Alt](https://site.logo.png)'}
copy_images(cell, Path('01_export.ipynb'), get_config().path("doc_path"))
test_eq(cell["source"], 'Text\n![Alt](https://site.logo.png)')
cell = {'cell_type': 'markdown', 'source':'Text\n![Alt](https://site.logo.png "caption")'}
copy_images(cell, Path('01_export.ipynb'), get_config().path("doc_path"))
test_eq(cell["source"], 'Text\n![Alt](https://site.logo.png "caption")')
#|export
def _relative_to(path1, path2):
p1,p2 = Path(path1).absolute().parts,Path(path2).absolute().parts
i=0
while i 0]
#|export
_re_title_summary = re.compile(r"""
# Catches the title and summary of the notebook, presented as # Title > summary, with title in group 1 and summary in group 2
^\s* # Beginning of text followed by any number of whitespace
\#\s+ # # followed by one or more of whitespace
([^\n]*) # Catching group for any character except a new line
\n+ # One or more new lines
>[ ]* # > followed by any number of whitespace
([^\n]*) # Catching group for any character except a new line
""", re.VERBOSE)
_re_title_only = re.compile(r"""
# Catches the title presented as # Title without a summary
^\s* # Beginning of text followed by any number of whitespace
\#\s+ # # followed by one or more of whitespace
([^\n]*) # Catching group for any character except a new line
(?:\n|$) # New line or end of text
""", re.VERBOSE)
_re_properties = re.compile(r"""
^-\s+ # Beginning of a line followed by - and at least one space
(.*?) # Any pattern (shortest possible)
\s*:\s* # Any number of whitespace, :, any number of whitespace
(.*?)$ # Any pattern (shortest possible) then end of line
""", re.MULTILINE | re.VERBOSE)
_re_mdlinks = re.compile(r"\[(.+)]\((.+)\)", re.MULTILINE)
#|export
def _md2html_links(s):
'Converts markdown links to html links'
return _re_mdlinks.sub(r"\1", s)
#|export
def get_metadata(cells):
"Find the cell with title and summary in `cells`."
for i,cell in enumerate(cells):
if cell['cell_type'] == 'markdown':
match = _re_title_summary.match(cell['source'])
if match:
cells.pop(i)
attrs = {k:v for k,v in _re_properties.findall(cell['source'])}
return {'keywords': 'fastai',
'summary' : _md2html_links(match.groups()[1]),
'title' : match.groups()[0],
**attrs}
elif _re_title_only.search(cell['source']) is not None:
title = _re_title_only.search(cell['source']).groups()[0]
cells.pop(i)
attrs = {k:v for k,v in _re_properties.findall(cell['source'])}
return {'keywords': 'fastai',
'title' : title,
**attrs}
return {'keywords': 'fastai',
'title' : 'Title'}
tst_nb = read_nb('00_export.ipynb')
test_eq(get_metadata(tst_nb['cells']), {
'keywords': 'fastai',
'summary': 'The functions that transform notebooks in a library',
'title': 'Export to modules'})
#The cell with the metada is popped out, so if we do it a second time we get the default.
test_eq(get_metadata(tst_nb['cells']), {'keywords': 'fastai', 'title' : 'Title'})
#|hide
#test with title only
test_eq(get_metadata([{'cell_type': 'markdown', 'source': '# Awesome title'}]),
{'keywords': 'fastai', 'title': 'Awesome title'})
#|hide
text = r"""
[This](https://nbdev.fast.ai) goes to docs.
This [one:here](00_export.ipynb) goes to a local nb.
\n[And-this](http://dev.fast.ai/) goes to fastai docs
"""
res = """
This goes to docs.\nThis one:here goes to a local nb. \n\\nAnd-this goes to fastai docs
"""
test_eq(_md2html_links(text), res)
#|hide
cells = [{'cell_type': 'markdown', 'source': "# Title\n\n> s\n\n- toc: false"}]
test_eq(get_metadata(cells), {'keywords': 'fastai', 'summary': 's', 'title': 'Title', 'toc': 'false'})
#|export
_re_mod_export = _mk_flag_re("export[s]?", 1,
"Matches any line with #export or #exports with a module name and catches it in group 1")
def _gather_export_mods(cells):
res = []
for cell in cells:
tst = check_re(cell, _re_mod_export)
if tst is not None: res.append(tst.groups()[0])
return res
#|hide
cells = [
{'cell_type': 'markdown', 'source': '#export ignored'},
{'cell_type': 'code', 'source': '#export'},
{'cell_type': 'code', 'source': '#export normal'},
{'cell_type': 'code', 'source': '# exports show'},
{'cell_type': 'code', 'source': '# exporti hidden'},
{'cell_type': 'code', 'source': '#export\n@call_parse'},
{'cell_type': 'code', 'source': '#export \n@delegates(concurrent.futures.ProcessPoolExecutor)'}
]
test_eq(_gather_export_mods(cells), ['normal', 'show'])
#|export
# match any cell containing a zero indented import from the current lib
_re_lib_import = ReLibName(r"^from LIB_NAME\.", re.MULTILINE)
# match any cell containing a zero indented import
_re_import = re.compile(r"^from[ \t]+\S+[ \t]+import|^import[ \t]", re.MULTILINE)
# match any cell containing a zero indented call to notebook2script
_re_notebook2script = re.compile(r"^notebook2script\(", re.MULTILINE)
#|hide
for cell in [nbformat.v4.new_code_cell(s, metadata={'exp': exp}) for exp,s in [
(True, 'show_doc(Tensor.p)'),
(True, ' show_doc(Tensor.p)'),
(True, 'if something:\n show_doc(Tensor.p)'),
(False, '# show_doc(Tensor.p)'),
(True, '# comment \n show_doc(Tensor.p)'),
(True, '"""\nshow_doc(Tensor.p)\n"""'),
(True, 'import torch\nshow_doc(Tensor.p)'),
(False,'class Ex(ExP):\n"An `ExP` that ..."\ndef preprocess_cell(self, cell, resources, index):\n'),
(False, 'from somewhere import something'),
(False, 'from '),
(False, 'import re'),
(False, 'import '),
(False, 'try: from PIL import Image\except: pass'),
(False, 'from PIL import Image\n@patch\ndef p(x:Image):\n pass'),
(False, '@patch\ndef p(x:Image):\n pass\nfrom PIL import Image')]]:
exp = cell.metadata.exp
assert exp == bool(check_re_multi(cell, [_re_show_doc, _re_lib_import.re])), f'expected {exp} for {cell}'
#|hide
for cell in [nbformat.v4.new_code_cell(s, metadata={'exp': exp}) for exp,s in [
(False, 'show_doc(Tensor.p)'),
(True, 'import torch\nshow_doc(Tensor.p)'),
(False,'class Ex(ExP):\n"An `ExP` that ..."\ndef preprocess_cell(self, cell, resources, index):\n'),
(False, ' from somewhere import something'),
(True, 'from somewhere import something'),
(False, 'from '),
(False, 'select * \nfrom database'),
(False, ' import re'),
(True, 'import re'),
(True, 'import '),
(False, 'try: from PIL import Image\except: pass'),
(True, 'from PIL import Image\n@patch\ndef p(x:Image):\n pass'),
(True, '@patch\ndef p(x:Image):\n pass\nfrom PIL import Image')]]:
exp = cell.metadata.exp
assert exp == bool(check_re(cell, _re_import)), f'expected {exp} for {cell}'
#|hide
for cell in [nbformat.v4.new_code_cell(s, metadata={'exp': exp}) for exp,s in [
(False, 'show_doc(Tensor.p)'),
(False, 'notebook2script'),
(False, '#notebook2script()'),
(True, 'notebook2script()'),
(True, 'notebook2script(anything at all)')]]:
exp = cell.metadata.exp
assert exp == bool(check_re(cell, _re_notebook2script)), f'expected {exp} for {cell}'
#|export
def _non_comment_code(s):
if re.match(r'\s*#', s): return False
if _re_import.findall(s) or _re_lib_import.re.findall(s): return False
return re.match(r'\s*\w', s)
#|export
class ExecuteShowDocPreprocessor(ExecutePreprocessor):
"An `ExecutePreprocessor` that only executes `show_doc` and `import` cells"
def preprocess_cell(self, cell, resources, index):
if not check_re(cell, _re_notebook2script):
if check_re(cell, _re_show_doc):
return super().preprocess_cell(cell, resources, index)
elif check_re_multi(cell, [_re_import, _re_lib_import.re]):
if check_re_multi(cell, [_re_export, 'show_doc', '^\s*#\s*import']):
# r = list(filter(_non_comment_code, cell['source'].split('\n')))
# if r: print("You have import statements mixed with other code", r)
return super().preprocess_cell(cell, resources, index)
# try: return super().preprocess_cell(cell, resources, index)
# except: pass
return cell, resources
#|export
def _import_show_doc_cell(mods=None):
"Add an import show_doc cell."
source = f"from nbdev.showdoc import show_doc"
if mods is not None:
for mod in mods: source += f"\nfrom {get_config().lib_name}.{mod} import *"
return {'cell_type': 'code',
'execution_count': None,
'metadata': {'hide_input': True},
'outputs': [],
'source': source}
def execute_nb(nb, mod=None, metadata=None, show_doc_only=True):
"Execute `nb` (or only the `show_doc` cells) with `metadata`"
mods = ([] if mod is None else [mod]) + _gather_export_mods(nb['cells'])
nb['cells'].insert(0, _import_show_doc_cell(mods))
ep_cls = ExecuteShowDocPreprocessor if show_doc_only else ExecutePreprocessor
ep = ep_cls(timeout=600)
metadata = metadata or {}
pnb = nbformat.from_dict(nb)
ep.preprocess(pnb, metadata)
return pnb
#|export
_re_cite = re.compile(r"(\\cite{)([^}]*)(})", re.MULTILINE | re.VERBOSE) # Catches citations used with `\cite{}`
#|export
def _textcite2link(text):
citations = _re_cite.finditer(text)
out = []
start_pos = 0
for cit_group in citations:
cit_pos_st = cit_group.span()[0]
cit_pos_fin = cit_group.span()[1]
out.append(text[start_pos:cit_pos_st])
out.append('[')
cit_group = cit_group[2].split(',')
for i, cit in enumerate(cit_group):
cit=cit.strip()
out.append(f"""{cit}""")
if i != len(cit_group) - 1:
out.append(',')
out.append(']')
start_pos = cit_pos_fin
out.append(text[start_pos:])
return ''.join(out)
#|export
def cite2link(cell):
'''Creates links from \cite{} to Reference section generated by jupyter_latex_envs'''
if cell['cell_type'] == 'markdown': cell['source'] = _textcite2link(cell['source'])
return cell
#|hide
cell = {'cell_type': 'markdown', 'source': r"""This is cited multireference \cite{Frob1, Frob3}.
And single \cite{Frob2}."""}
expected=r"""This is cited multireference [Frob1,Frob3].
And single [Frob2]."""
test_eq(cite2link(cell)["source"], expected)
#slow
fake_nb = {k:v for k,v in tst_nb.items() if k != 'cells'}
fake_nb['cells'] = [tst_nb['cells'][0].copy()] + added_cells
fake_nb = execute_nb(fake_nb, mod='export')
assert len(fake_nb['cells'][-2]['outputs']) > 0
#|export
def write_tmpl(tmpl, nms, cfg, dest):
"Write `tmpl` to `dest` (if missing) filling in `nms` in template using dict `cfg`"
if dest.exists(): return
vs = {o:cfg.d[o] for o in nms.split()}
outp = tmpl.format(**vs)
dest.mk_write(outp)
#|export
def write_tmpls():
"Write out _config.yml and _data/topnav.yml using templates"
cfg = get_config()
path = Path(cfg.get('doc_src_path', cfg.path("doc_path")))
write_tmpl(config_tmpl, 'user lib_name title copyright description recursive', cfg, path/'_config.yml')
write_tmpl(topnav_tmpl, 'host git_url', cfg, path/'_data'/'topnav.yml')
write_tmpl(makefile_tmpl, 'nbs_path lib_name', cfg, cfg.config_file.parent/'Makefile')
#|export
@call_parse
def nbdev_build_lib(
fname:str=None, # A notebook name or glob to convert
bare:store_true=False # Omit nbdev annotation comments (may break some functionality).
):
"Export notebooks matching `fname` to python modules"
if fname is None: write_tmpls()
notebook2script(fname=fname, bare=bare)
__file__ = str(get_config().path("lib_path")/'export2html.py')
#|export
def nbdev_exporter(cls=HTMLExporter, template_file=None):
cfg = traitlets.config.get_config()
exporter = cls(cfg)
exporter.exclude_input_prompt=True
exporter.exclude_output_prompt=True
exporter.anchor_link_text = ' '
exporter.template_file = 'jekyll.tpl' if template_file is None else template_file
exporter.template_paths.append(str(Path(__file__).parent/'templates'))
return exporter
#|export
process_cells = [remove_fake_headers, remove_hidden, remove_empty]
process_cell = [hide_cells, collapse_cells, remove_widget_state, add_jekyll_notes, escape_latex, cite2link]
#|export
def _nb2htmlfname(nb_path, dest=None):
if dest is None: dest = get_config().path("doc_path")
return Path(dest)/re_digits_first.sub('', nb_path.with_suffix('.html').name)
#|hide
test_eq(_nb2htmlfname(Path('00a_export.ipynb')), get_config().path("doc_path")/'export.html')
test_eq(_nb2htmlfname(Path('export.ipynb')), get_config().path("doc_path")/'export.html')
test_eq(_nb2htmlfname(Path('00ab_export_module_1.ipynb')), get_config().path("doc_path")/'export_module_1.html')
test_eq(_nb2htmlfname(Path('export.ipynb'), '.'), Path('export.html'))
#|export
def convert_nb(fname, cls=HTMLExporter, template_file=None, exporter=None, dest=None, execute=True):
"Convert a notebook `fname` to html file in `dest_path`."
fname = Path(fname).absolute()
nb = read_nb(fname)
meta_jekyll = get_metadata(nb['cells'])
meta_jekyll['nb_path'] = str(fname.relative_to(get_config().path("lib_path").parent))
cls_lvl = find_default_level(nb['cells'])
mod = find_default_export(nb['cells'])
nb['cells'] = compose(*process_cells,partial(add_show_docs, cls_lvl=cls_lvl))(nb['cells'])
_func = compose(partial(copy_images, fname=fname, dest=get_config().path("doc_path")), *process_cell, treat_backticks)
nb['cells'] = [_func(c) for c in nb['cells']]
if execute: nb = execute_nb(nb, mod=mod)
nb['cells'] = [clean_exports(c) for c in nb['cells']]
if exporter is None: exporter = nbdev_exporter(cls=cls, template_file=template_file)
with open(_nb2htmlfname(fname, dest=dest),'w') as f:
f.write(exporter.from_notebook_node(nb, resources=meta_jekyll)[0])
#|export
def _notebook2html(fname, cls=HTMLExporter, template_file=None, exporter=None, dest=None, execute=True):
time.sleep(random.random())
print(f"converting: {fname}")
try:
convert_nb(fname, cls=cls, template_file=template_file, exporter=exporter, dest=dest, execute=execute)
return True
except Exception as e:
print(e)
return False
#|export
def notebook2html(fname=None, force_all=False, n_workers=None, cls=HTMLExporter, template_file=None,
exporter=None, dest=None, pause=0, execute=True):
"Convert all notebooks matching `fname` to html files"
files = nbglob(fname)
if len(files)==1:
force_all = True
if n_workers is None: n_workers=0
if not force_all:
# only rebuild modified files
files,_files = [],files.copy()
for fname in _files:
fname_out = _nb2htmlfname(Path(fname).absolute(), dest=dest)
if not fname_out.exists() or os.path.getmtime(fname) >= os.path.getmtime(fname_out):
files.append(fname)
if len(files)==0: print("No notebooks were modified")
else:
if sys.platform == "win32": n_workers = 0
passed = parallel(_notebook2html, files, n_workers=n_workers, cls=cls,
template_file=template_file, exporter=exporter, dest=dest, pause=pause, execute=execute)
if not all(passed):
msg = "Conversion failed on the following:\n"
print(msg + '\n'.join([f.name for p,f in zip(passed,files) if not p]))
with tempfile.TemporaryDirectory() as d:
print(d)
notebook2html('01_sync.ipynb', dest='.', n_workers=0);
#|hide
# Test when an argument is given to notebook2html
with tempfile.TemporaryDirectory() as d:
p1 = Path(d).joinpath('sync.html')
notebook2html('01_sync.ipynb', dest=d, n_workers=0);
assert p1.exists()
#slow
#|hide
# Test when no argument is given to notebook2html
with tempfile.TemporaryDirectory() as d:
dest_files = [_nb2htmlfname(Path(f), dest=d) for f in nbglob()]
[f.unlink() for f in dest_files if f.exists()]
notebook2html(fname=None, dest=d);
for f in dest_files: assert f.exists(), f
#|hide
#notebook2html(force_all=True)
#|export
def convert_md(fname, dest_path, img_path='docs/images/', jekyll=True):
"Convert a notebook `fname` to a markdown file in `dest_path`."
fname = Path(fname).absolute()
if not img_path: img_path = fname.stem + '_files/'
Path(img_path).mkdir(exist_ok=True, parents=True)
nb = read_nb(fname)
meta_jekyll = get_metadata(nb['cells'])
try: meta_jekyll['nb_path'] = str(fname.relative_to(get_config().path("lib_path").parent))
except: meta_jekyll['nb_path'] = str(fname)
nb['cells'] = compose(*process_cells)(nb['cells'])
nb['cells'] = [compose(partial(adapt_img_path, fname=fname, dest=dest_path, jekyll=jekyll), *process_cell)(c)
for c in nb['cells']]
fname = Path(fname).absolute()
dest_name = fname.with_suffix('.md').name
exp = nbdev_exporter(cls=MarkdownExporter, template_file='jekyll-md.tpl' if jekyll else 'md.tpl')
export = exp.from_notebook_node(nb, resources=meta_jekyll)
md = export[0]
for ext in ['png', 'svg']:
md = re.sub(r'!\['+ext+'\]\((.+)\)', '!['+ext+'](' + img_path + '\\1)', md)
with (Path(dest_path)/dest_name).open('w') as f: f.write(md)
if hasattr(export[1]['outputs'], 'items'):
for n,o in export[1]['outputs'].items():
with open(Path(dest_path)/img_path/n, 'wb') as f: f.write(o)
#|hide
def _test_md(fn):
fn,dest = Path(fn),Path().absolute().parent
try: convert_md(fn, dest, jekyll=False)
finally: (dest/f'{fn.stem}.md').unlink()
#|hide
_test_md('index.ipynb')
# `export[1]['outputs']` will be a `str` if the notebook has no markdown cells to convert.
# e.g. the nb could have a single jekyll markdown cell or just code cells ...
_test_md(f'../tests/single-cell-index.ipynb')
#|export
_re_att_ref = re.compile(r' *!\[(.*)\]\(attachment:image.png(?: "(.*)")?\)')
t = '![screenshot](attachment:image.png)'
test_eq(_re_att_ref.match(t).groups(), ('screenshot', None))
t = '![screenshot](attachment:image.png "Deploying to Binder")'
test_eq(_re_att_ref.match(t).groups(), ('screenshot', "Deploying to Binder"))
#|export
try: from PIL import Image
except: pass # Only required for _update_att_ref
#|export
_tmpl_img = ''
def _update_att_ref(line, path, img):
m = _re_att_ref.match(line)
if not m: return line
alt,title = m.groups()
w = img.size[0]
if alt=='screenshot': w //= 2
if not title: title = "TK: add title"
return _tmpl_img.format(title=title, width=str(w), id='TK: add it', name=str(path))
#|export
def _nb_detach_cell(cell, dest, use_img):
att,src = cell['attachments'],cell['source']
mime,img = first(first(att.values()).items())
ext = mime.split('/')[1]
for i in range(99999):
p = dest/(f'att_{i:05d}.{ext}')
if not p.exists(): break
img = b64decode(img)
p.write_bytes(img)
del(cell['attachments'])
if use_img: return [_update_att_ref(o,p,Image.open(p)) for o in src]
else: return [o.replace('attachment:image.png', str(p)) for o in src]
#|export
def _nbdev_detach(path_nb, dest="", use_img=False, replace=True):
path_nb = Path(path_nb)
if not dest: dest = f'{path_nb.stem}_files'
dest = Path(dest)
dest.mkdir(exist_ok=True, parents=True)
j = json.load(path_nb.open())
atts = [o for o in j['cells'] if 'attachments' in o]
for o in atts: o['source'] = _nb_detach_cell(o, dest, use_img)
if atts and replace: json.dump(j, path_nb.open('w'))
if not replace: return j
@call_parse
def nbdev_detach(path_nb:Param("Path to notebook"),
dest:str="", # Destination folder
use_img:bool_arg=False, # Convert markdown images to img tags
replace:bool_arg=True # Write replacement notebook back to `path_bn`
):
"Export cell attachments to `dest` and update references"
_nbdev_detach(path_nb, dest, use_img, replace)
#|export
_re_index = re.compile(r'^(?:\d*_|)index\.ipynb$')
#|export
def make_readme():
"Convert the index notebook to README.md"
index_fn = None
for f in get_config().path("nbs_path").glob('*.ipynb'):
if _re_index.match(f.name): index_fn = f
assert index_fn is not None, "Could not locate index notebook"
print(f"converting {index_fn} to README.md")
convert_md(index_fn, get_config().config_file.parent, jekyll=False)
n = get_config().config_file.parent/index_fn.with_suffix('.md').name
shutil.move(n, get_config().config_file.parent/'README.md')
if Path(get_config().config_file.parent/'PRE_README.md').is_file():
with open(get_config().config_file.parent/'README.md', 'r') as f: readme = f.read()
with open(get_config().config_file.parent/'PRE_README.md', 'r') as f: pre_readme = f.read()
with open(get_config().config_file.parent/'README.md', 'w') as f: f.write(f'{pre_readme}\n{readme}')
#|export
@call_parse
def nbdev_build_docs(
fname:str=None, # A notebook name or glob to convert
force_all:bool_arg=False, # Rebuild even notebooks that havent changed
mk_readme:bool_arg=True, # Also convert the index notebook to README
n_workers:int=None, # Number of workers to use
pause:float=0.5 # Pause time (in secs) between notebooks to avoid race conditions
):
"Build the documentation by converting notebooks matching `fname` to html"
notebook2html(fname=fname, force_all=force_all, n_workers=n_workers, pause=pause)
if fname is None: make_sidebar()
if mk_readme: make_readme()
#|export
@call_parse
def nbdev_nb2md(
fname:str, # A notebook file name to convert
dest:str='.', # The destination folder
img_path:None="", # Folder to export images to
jekyll:bool_arg=False # To use jekyll metadata for your markdown file or not
):
"Convert the notebook in `fname` to a markdown file"
_nbdev_detach(fname, dest=img_path)
convert_md(fname, dest, jekyll=jekyll, img_path=img_path)
#|export
import time,random,warnings
#|export
def _leaf(k,v):
url = 'external_url' if "http" in v else 'url'
#if url=='url': v=v+'.html'
return {'title':k, url:v, 'output':'web,pdf'}
#|export
_k_names = ['folders', 'folderitems', 'subfolders', 'subfolderitems']
def _side_dict(title, data, level=0):
k_name = _k_names[level]
level += 1
res = [(_side_dict(k, v, level) if isinstance(v,dict) else _leaf(k,v))
for k,v in data.items()]
return ({k_name:res} if not title
else res if title.startswith('empty')
else {'title': title, 'output':'web', k_name: res})
#|export
_re_catch_title = re.compile('^title\s*:\s*(\S+.*)$', re.MULTILINE)
#|export
def _get_title(fname):
"Grabs the title of html file `fname`"
with open(fname, 'r') as f: code = f.read()
src = _re_catch_title.search(code)
return fname.stem if src is None else src.groups()[0]
#|hide
test_eq(_get_title(get_config().path("doc_path")/'export.html'), "Export to modules")
#|export
def _create_default_sidebar():
"Create the default sidebar for the docs website"
dic = {"Overview": "/"}
files = nbglob()
fnames = [_nb2htmlfname(f) for f in sorted(files)]
names = [f for f in fnames if f.stem!='index']
for t,f in groupby(names, _get_title).items():
if len(f) > 1: print(f'WARNING: The title: "{t}" appears in {len(f)} pages:\n\t\t{L(f).map(str)}')
dic.update({_get_title(f):f.name if get_config().host=='github' else f.with_suffix('').name for f in fnames if f.stem!='index'})
return dic
#|export
def create_default_sidebar():
"Create the default sidebar for the docs website"
dic = {get_config().lib_name: _create_default_sidebar()}
json.dump(dic, open(get_config().path("doc_path")/'sidebar.json', 'w'), indent=2)
#|export
def make_sidebar():
"Making sidebar for the doc website form the content of `doc_folder/sidebar.json`"
cfg = get_config()
if not (cfg.path("doc_path")/'sidebar.json').exists() or cfg.get('custom_sidebar', 'False') == 'False':
create_default_sidebar()
sidebar_d = json.load(open(cfg.path("doc_path")/'sidebar.json', 'r'))
res = _side_dict('Sidebar', sidebar_d)
res = {'entries': [res]}
res_s = yaml.dump(res, default_flow_style=False)
res_s = res_s.replace('- subfolders:', ' subfolders:').replace(' - - ', ' - ')
res_s = f"""
#################################################
### THIS FILE WAS AUTOGENERATED! DO NOT EDIT! ###
#################################################
# Instead edit {'../../sidebar.json'}
"""+res_s
pth = cfg.path("doc_path")/'_data/sidebars/home_sidebar.yml'
pth.mk_write(res_s)
make_sidebar()
#|hide
from nbdev.export import *
notebook2script()