# jsonschema_diff/sphinx/directive.py"""Sphinx directive to embed a Rich‑rendered JSON‑schema diff as SVG.Usage:: .. jsonschemadiff:: old.json new.json :name: my_diff.svg # optional custom file name (".svg" can be omitted) :title: Schema Diff # title shown inside the virtual terminal tab :no-body: # hide diff body, keep legend only :no-legend: # hide legend, show body only :width: 80% # pass through to the resulting <img> tagAll options are optional; sensible defaults are applied when omitted."""from__future__importannotationsimporthashlibimportioimportshutilfrompathlibimportPathfromtypingimportCallable,List,Optionalfromdocutilsimportnodesfromdocutils.parsers.rstimportDirective,directivesfromrich.consoleimportConsole,Groupfromsphinx.errorsimportExtensionErrorfromsphinx.utilimportlogging__all__=["JsonSchemaDiffDirective"]LOGGER=logging.getLogger(__name__)
[docs]classJsonSchemaDiffDirective(Directive):"""Embed an SVG diff between two JSON‑schema files."""
[docs]defrun(self)->List[nodes.Node]:# noqa: D401env=self.state.document.settings.envsrcdir=Path(env.srcdir)old_path=(srcdir/self.arguments[0]).resolve()new_path=(srcdir/self.arguments[1]).resolve()ifnotold_path.exists()ornotnew_path.exists():raiseself.error(f"JSON‑schema file not found: {old_path} / {new_path}")# ------------------------------------------------------------------# Retrieve configured diff object from conf.pydiff=getattr(env.app.config,"jsonschema_diff",None)ifdiffisNone:raiseExtensionError("Define variable `jsonschema_diff` (JsonSchemaDiff instance) in conf.py.")fromjsonschema_diffimportJsonSchemaDiff# pylint: disable=import-outside-toplevelifnotisinstance(diff,JsonSchemaDiff):raiseExtensionError("`jsonschema_diff` is not a JsonSchemaDiff instance.")# ------------------------------------------------------------------# Produce Rich renderablesdiff.compare(str(old_path),str(new_path))renderables:list=[]body=diff.rich_render()if"no-body"notinself.options:renderables.append(body)if"no-legend"notinself.optionsandhasattr(diff,"rich_legend"):renderables.append(diff.rich_legend(diff.last_compare_list))ifnotrenderables:return[]# ------------------------------------------------------------------# Use Rich to create SVGconsole=Console(record=True,width=self._CONSOLE_WIDTH,file=io.StringIO())console.print(Group(*renderables))export_kwargs={"title":self.options.get("title","Rich"),"clear":False,}svg_code=console.export_svg(**export_kwargs)# ------------------------------------------------------------------# Save SVG to _static/jsonschema_diffstatic_dir=Path(env.app.srcdir)/self._STATIC_SUBDIRifnothasattr(env.app,"_jsonschema_diff_cleaned"):shutil.rmtree(static_dir,ignore_errors=True)env.app._jsonschema_diff_cleaned=Truestatic_dir.mkdir(parents=True,exist_ok=True)svg_name=self._make_svg_name(old_path,new_path,console.export_text)svg_path=static_dir/svg_namesvg_path.write_text(svg_code,encoding="utf-8")# ------------------------------------------------------------------# Insert <img> node with correct relative URIdoc_depth=env.docname.count("/")uri_prefix="../"*doc_depthimg_uri=f"{uri_prefix}_static/jsonschema_diff/{svg_name}"img_node=nodes.image(uri=img_uri,alt=f"diff {old_path.name}")if"width"inself.options:img_node["width"]=self.options["width"]return[img_node]
# ------------------------------------------------------------------def_make_svg_name(self,old_path:Path,new_path:Path,export_text:Callable,)->str:"""Return custom name (if provided) or deterministic hash‑based name."""custom_name:Optional[str]=self.options.get("name")ifcustom_nameandnotcustom_name.lower().endswith(".svg"):custom_name+=".svg"ifcustom_name:returncustom_namedigest=hashlib.md5(export_text(clear=False).encode()).hexdigest()[:8]returnf"{old_path.stem}-{new_path.stem}-{digest}.svg"