import sys
import nbformat
from nbdev.imports import *
from nbdev.process import *
from nbdev.processors import NBProcessor, mk_cell, populate_language, add_show_docs, insert_warning,\
\
strip_ansi, hide_line, filter_stream_, rm_header_dash,
clean_show_doc, exec_show_docs, rm_export, clean_magics, hide_, add_links, strip_hidden_metadatafrom pathlib import Path
import yaml
import re
from execnb.nbio import dict2nb,loads
We need to import required nbdev processors for notebook for minimal rendering.
A few changes required
Nbdev has this notion of path_ of notebook which it adds as metadata on each notebook in FrontmatterProc. Since we work with stdout, it is difficult to know the same when adding an ipynb-filter. So we need to overwrite FrontMatterProc
Not all nbdev processors are required for pure quarto site , so we need to add only the once we use
Imports
FrontMatterProc
=r'''^---\s*
_RE_FM_BASE(.*?\S+.*?)
---\s*'''
= re.compile(_RE_FM_BASE+'$', flags=re.DOTALL)
_re_fm_nb = re.compile(_RE_FM_BASE, flags=re.DOTALL)
_re_fm_md
def _fm2dict(s:str, nb=True):
"Load YAML frontmatter into a `dict`"
= _re_fm_nb if nb else _re_fm_md
re_fm = re_fm.search(s.strip())
match return yaml.safe_load(match.group(1)) if match else {}
def _md2dict(s:str):
"Convert H1 formatted markdown cell to frontmatter dict"
if '#' not in s: return {}
= re.search(r'^#\s+(\S.*?)\s*$', s, flags=re.MULTILINE)
m if not m: return {}
= {'title': m.group(1)}
res = re.search(r'^>\s+(\S.*?)\s*$', s, flags=re.MULTILINE)
m if m: res['description'] = m.group(1)
= re.findall(r'^-\s+(\S.*:.*\S)\s*$', s, flags=re.MULTILINE)
r if r:
try: res.update(yaml.safe_load('\n'.join(r)))
except Exception as e: warn(f'Failed to create YAML dict for:\n{r}\n\n{e}\n')
return res
def _dict2fm(d): return f'---\n{yaml.dump(d)}\n---\n\n'
def _insertfm(nb, fm): nb.cells.insert(0, mk_cell(_dict2fm(fm), 'raw'))
class MyFrontmatterProc(Processor):
"A YAML and formatted-markdown frontmatter processor"
def begin(self): self.fm = getattr(self.nb, 'frontmatter_', {})
def _update(self, f, cell):
= cell.get('source')
s if not s: return
= f(s)
d if not d: return
self.fm.update(d)
= None
cell.source
def cell(self, cell):
if cell.cell_type=='raw': self._update(_fm2dict, cell)
elif cell.cell_type=='markdown' and 'title' not in self.fm: self._update(_md2dict, cell)
def end(self):
self.nb.frontmatter_ = self.fm
if not self.fm: return
self.nb, self.fm) _insertfm(
Filter
def filter_nb():
= [MyFrontmatterProc, populate_language, insert_warning,
procs
strip_ansi, hide_line, filter_stream_, rm_header_dash,
clean_magics, hide_, strip_hidden_metadata]= sys.stdin.read()
nb_txt = dict2nb(loads(nb_txt))
nb = NBProcessor(nb=nb, procs=procs)
nbp
nbp.process()
nbformat.write(nbp.nb, sys.stdout)
filter_nb()
--------------------------------------------------------------------------- JSONDecodeError Traceback (most recent call last) Cell In[6], line 13 10 nbp.process() 11 nbformat.write(nbp.nb, sys.stdout) ---> 13 filter_nb() Cell In[6], line 8, in filter_nb() 4 procs = [MyFrontmatterProc, populate_language, insert_warning, 5 strip_ansi, hide_line, filter_stream_, rm_header_dash, 6 clean_magics, hide_, strip_hidden_metadata] 7 nb_txt = sys.stdin.read() ----> 8 nb = dict2nb(loads(nb_txt)) 9 nbp = NBProcessor(nb=nb, procs=procs) 10 nbp.process() File /opt/homebrew/Caskroom/miniforge/base/lib/python3.10/json/__init__.py:346, in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw) 341 s = s.decode(detect_encoding(s), 'surrogatepass') 343 if (cls is None and object_hook is None and 344 parse_int is None and parse_float is None and 345 parse_constant is None and object_pairs_hook is None and not kw): --> 346 return _default_decoder.decode(s) 347 if cls is None: 348 cls = JSONDecoder File /opt/homebrew/Caskroom/miniforge/base/lib/python3.10/json/decoder.py:337, in JSONDecoder.decode(self, s, _w) 332 def decode(self, s, _w=WHITESPACE.match): 333 """Return the Python representation of ``s`` (a ``str`` instance 334 containing a JSON document). 335 336 """ --> 337 obj, end = self.raw_decode(s, idx=_w(s, 0).end()) 338 end = _w(s, end).end() 339 if end != len(s): File /opt/homebrew/Caskroom/miniforge/base/lib/python3.10/json/decoder.py:355, in JSONDecoder.raw_decode(self, s, idx) 353 obj, end = self.scan_once(s, idx) 354 except StopIteration as err: --> 355 raise JSONDecodeError("Expecting value", s, err.value) from None 356 return obj, end JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Add filter to _quarto.yml
ipynb-filters:
- nbdev_filter.py
project:
type: website
pre-render: custom2qmd.py
# post-render:
# - notes2site.py
# - app2site.py
preview:
host: '0.0.0.0'
port: 3000
browser: false
# twitter-card: true
Modifying Github Action
For making quarto work with libraries outside python STL we will need to update python environment with additional dependencies. For reasons unknown to me, this modification only works after quarto setup action. Relevant snippet from my publish file
- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Install Python and Dependencies
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- run: pip install jupyter
- run: pip install -r requirements.txt
- name: Render and Publish
uses: quarto-dev/quarto-actions/publish@v2
with:
target: netlify
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}