#|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('alt') test_eq(t['width'], '700') test_eq(t['src' ], 'src') t['width'] = '600' test_eq(h.show(), 'alt') #|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 = '{title}' 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()