From 2ac2664c692de467e24c550439ada774f40b3408 Mon Sep 17 00:00:00 2001 From: Greg Wilson <greg.wilson@plot.ly> Date: Mon, 2 Dec 2024 09:31:49 -0500 Subject: [PATCH 1/4] feat: use MkDocs to generate documentation from plot-schema.json 1. Copy `make_ref_pages.py` from graphing-library-docs repository to create `bin/make_ref_pages.py` and then refactor to generate Markdown stubs for documentation pages. 1. Write `bin/gen.py` to generate HTML pages from the Markdown stubs using the Jinja templates in the `theme` directory for debugging. 1. Convert Jekyll templates from graphing-library-docs repository to Jinja and store in `theme` directory. 1. Write `requirements.txt` file to install required Python packages. - `beautifulsoup4`: for HTML manipulation - `html5validator`: to validate generated HTML - `jinja2`: for manual template expansion - `mkdocs`: for website generation - `mkdocs-exclude`: to exclude `.jinja` files from generation - `python-frontmatter`: for manual template expansion - `ruff`: for checking `bin` scripts 1. Write `Makefile` to automate build and test. 1. Modify documentation comments in some JavaScript files to remove stray backticks. Notes: 1. As of the time of this commit, the `mkdocs_data_loader` plugin must be installed directly from a `.whl` file or similar. It will be added to `requirements.txt` once the plugin is on PyPI. 1. Jinja does not expand directives in Markdown files; it only expands those it finds in template `.jinja` files. Because of this, the Markdown stubs generated by `bin/make_ref_pages.py` have YAML headers but no bodies. 1. The logic to set the `details` variable in `theme/main.jinja` is copied from the original Jekyll templates and modified by trial and error to produce (what appears to be) the right answer. If pages contain extra information, or if information is missing, the most likely cause is an error here. 1. At the time of this commit, no styling is applied to the generated pages and they do not contain headers, footers, or navigation. All of this should be added once a general site theme is developed. 1. `bin/gen.py` defines a `backtick` filter to convert backtick'd pieces of text to `<code>` blocks. This is currently *not* used in the Jinja templates because (a) the existing documentation includes the backticks as-is and (b) I couldn't be bothered to figure out how to add it to MkDocs. --- .gitignore | 5 + Makefile | 95 +++++++++++++ bin/gen.py | 97 +++++++++++++ bin/make_ref_pages.py | 125 +++++++++++++++++ bin/plugins.py | 22 +++ bin/pretty-html.py | 6 + mkdocs.yml | 18 +++ requirements.txt | 7 + src/components/errorbars/attributes.js | 2 +- src/plots/cartesian/layout_attributes.js | 2 +- src/plots/polar/layout_attributes.js | 2 +- src/traces/carpet/axis_attributes.js | 2 +- theme/attribute.jinja | 7 + theme/block.jinja | 168 +++++++++++++++++++++++ theme/global.jinja | 14 ++ theme/main.jinja | 30 ++++ theme/trace.jinja | 22 +++ 17 files changed, 620 insertions(+), 4 deletions(-) create mode 100644 Makefile create mode 100644 bin/gen.py create mode 100644 bin/make_ref_pages.py create mode 100644 bin/plugins.py create mode 100644 bin/pretty-html.py create mode 100644 mkdocs.yml create mode 100644 requirements.txt create mode 100644 theme/attribute.jinja create mode 100644 theme/block.jinja create mode 100644 theme/global.jinja create mode 100644 theme/main.jinja create mode 100644 theme/trace.jinja diff --git a/.gitignore b/.gitignore index 02e1f10c3e7..25133ea4674 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,8 @@ tags !.circleci !.gitignore !.npmignore + +docs +ref_pages +__pycache__ +tmp diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..eca2626bb5b --- /dev/null +++ b/Makefile @@ -0,0 +1,95 @@ +all: commands + +BUNDLE=build/plotly.js +OUT_DIR=docs +SCHEMA=test/plot-schema.json +REF_PAGES=ref_pages +THEME=theme + +## site: rebuild with MkDocs +site: + mkdocs build + +## pages: make all the pages +pages: + python bin/gen.py \ + --out docs \ + --schema ${SCHEMA} \ + --stubs ${REF_PAGES} \ + --theme ${THEME} + +## figure: generate docs for a figure (annotations.html) +figure: + python bin/gen.py \ + --crash \ + --out docs \ + --schema ${SCHEMA} \ + --stubs ${REF_PAGES} \ + --theme ${THEME} \ + annotations.md + +## global: generate docs for global (global.html) +global: + python bin/gen.py \ + --crash \ + --out docs \ + --schema ${SCHEMA} \ + --stubs ${REF_PAGES} \ + --theme ${THEME} \ + global.md + +## subplot: generate docs for a subplot (polar.html) +subplot: + python bin/gen.py \ + --crash \ + --out docs \ + --schema ${SCHEMA} \ + --stubs ${REF_PAGES} \ + --theme ${THEME} \ + polar.md + +## trace: generate docs for a trace (violin.html) +trace: + python bin/gen.py \ + --crash \ + --out docs \ + --schema ${SCHEMA} \ + --stubs ${REF_PAGES} \ + --theme ${THEME} \ + violin.md + +## stubs: make reference page stubs +stubs: ${SCHEMA} + python bin/make_ref_pages.py \ + --pages ${REF_PAGES} \ + --schema ${SCHEMA} \ + --verbose + +## validate: check the generated HTML +validate: + @html5validator --root docs + +## regenerate JavaScript schema +schema: + npm run schema dist + +## -------- : -------- +## commands: show available commands +# Note: everything with a leading double '##' and a colon is shown. +commands: + @grep -h -E '^##' ${MAKEFILE_LIST} \ + | sed -e 's/## //g' \ + | column -t -s ':' + +## find-subplots: use jq to find subplot objects +find-subplots: + @cat tmp/plot-schema-formatted.json | jq 'paths | select(.[length-1] == "_isSubplotObj")' + +## lint: check code and project +lint: + @ruff check bin + +## clean: erase all generated content +clean: + @find . -name '*~' -exec rm {} \; + @rm -rf ${REF_PAGES} ${OUT_DIR} diff --git a/bin/gen.py b/bin/gen.py new file mode 100644 index 00000000000..a7db0d1d2fa --- /dev/null +++ b/bin/gen.py @@ -0,0 +1,97 @@ +"""Build plotly.js documentation using jinja template.""" + +import argparse +import frontmatter +from jinja2 import Environment, FileSystemLoader +from jinja2.exceptions import TemplateError +import json +import markdown +from pathlib import Path +import sys + +import plugins + + +KEYS_TO_IGNORE = { + "_isSubplotObj", + "editType", + "role", +} +SUBPLOT = "_isSubplotObj" + + +def main(): + """Main driver.""" + opt = parse_args() + schema = json.loads(Path(opt.schema).read_text()) + env = Environment(loader=FileSystemLoader(opt.theme)) + env.filters["backtick"] = plugins.backtick + env.filters["debug"] = plugins.debug + all_pages = opt.page if opt.page else [p.name for p in Path(opt.stubs).glob("*.md")] + err_count = 0 + for page in all_pages: + if opt.crash: + render_page(opt, schema, env, page) + else: + try: + render_page(opt, schema, env, page) + except Exception as exc: + print(f"ERROR in {page}: {exc}", file=sys.stderr) + err_count += 1 + print(f"ERRORS: {err_count} / {len(all_pages)}") + + +def get_details(schema, page): + """Temporary hack to pull details out of schema and page header.""" + # Trace + if "full_name" not in page: + return page + + key = page["name"].split(".")[-1] + entry = schema["layout"]["layoutAttributes"][key] + + # Subplot + if SUBPLOT in entry: + return entry + + # Figure + return list(entry["items"].values())[0] + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument("--crash", action="store_true", help="crash on first error") + parser.add_argument("--out", help="name of output directory") + parser.add_argument("--schema", required=True, help="path to schema JSON") + parser.add_argument("--stubs", required=True, help="path to stubs directory") + parser.add_argument("--theme", required=True, help="path to theme directory") + parser.add_argument("page", nargs="...", help="name(s) of source file in stubs directory") + return parser.parse_args() + + +def render_page(opt, schema, env, page_name): + """Render a single page.""" + stem = Path(page_name).stem + loaded = frontmatter.load(Path(opt.stubs, page_name)) + metadata = loaded.metadata + assert "template" in metadata, f"page {page_name} does not specify 'template'" + content = loaded.content + details = get_details(schema, metadata) + template = env.get_template(metadata["template"]) + html = template.render( + page={"title": stem, "meta": metadata}, + config={"data": {"plot-schema": schema}}, + details=details, + keys_to_ignore=KEYS_TO_IGNORE, + content=content, + ) + if opt.out: + Path(opt.out, stem).mkdir(parents=True, exist_ok=True) + Path(opt.out, stem, "index.html").write_text(html) + else: + print(html) + + +if __name__ == "__main__": + main() diff --git a/bin/make_ref_pages.py b/bin/make_ref_pages.py new file mode 100644 index 00000000000..228a3b541c9 --- /dev/null +++ b/bin/make_ref_pages.py @@ -0,0 +1,125 @@ +"""Generate API reference pages from JSON metadata extracted from JavaScript source.""" + +import argparse +import json +from pathlib import Path +import sys + + +# Suffix for generated files. +SUFFIX = "md" + +# Attributes to document. +ATTRIBUTES = [ + "annotations", + "coloraxis", + "geo", + "images", + "map", + "mapbox", + "polar", + "scene", + "selections", + "shapes", + "sliders", + "smith", + "ternary", + "updatemenus", + "xaxis", + "yaxis", +] + +# Template for documentation of attributes. +ATTRIBUTE_TEMPLATE = """--- +template: attribute.jinja +permalink: /javascript/reference/{full_attribute_path}/ +name: {attribute} +full_name: {full_attribute} +description: Figure attribute reference for Plotly's JavaScript open-source graphing library. +parentlink: layout +block: layout +parentpath: layout +--- +""" + +# Documenting top-level layout. +GLOBAL_PAGE = """--- +template: global.jinja +permalink: /javascript/reference/layout/ +name: layout +description: Figure attribute reference for Plotly's JavaScript open-source graphing library. +parentlink: layout +block: layout +parentpath: layout +mustmatch: global +--- +""" + +# Template for documentation of trace. +TRACE_TEMPLATE = """--- +template: trace.jinja +permalink: /javascript/reference/{trace}/ +trace: {trace} +description: Figure attribute reference for Plotly's JavaScript open-source graphing library. +--- +""" + + +def main(): + """Main driver.""" + try: + opt = parse_args() + schema = json.loads(Path(opt.schema).read_text()) + make_global(opt) + for attribute in ATTRIBUTES: + make_attribute(opt, attribute) + for trace in schema["traces"]: + make_trace(opt, trace) + except AssertionError as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) + + +def make_attribute(opt, attribute): + """Write reference pages for attributes.""" + full_attribute = f"layout.{attribute}" + content = ATTRIBUTE_TEMPLATE.format( + full_attribute=full_attribute, + full_attribute_path=full_attribute.replace(".", "/"), + attribute=attribute, + ) + write_page(opt, "attribute", f"{attribute}.{SUFFIX}", content) + + +def make_global(opt): + """Make top-level 'global' page.""" + write_page(opt, "global", f"global.{SUFFIX}", GLOBAL_PAGE) + + + +def make_trace(opt, trace): + """Write reference page for trace.""" + content = TRACE_TEMPLATE.format(trace=trace,) + write_page(opt, "trace", f"{trace}.{SUFFIX}", content) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument("--pages", required=True, help="where to write generated page stubs") + parser.add_argument("--schema", required=True, help="path to plot schema file") + parser.add_argument("--verbose", action="store_true", help="report progress") + return parser.parse_args() + + +def write_page(opt, kind, page_name, content): + """Save a page.""" + output_path = Path(f"{opt.pages}/{page_name}") + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(content) + if opt.verbose: + print(f"{kind}: {output_path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/bin/plugins.py b/bin/plugins.py new file mode 100644 index 00000000000..da7dced80ae --- /dev/null +++ b/bin/plugins.py @@ -0,0 +1,22 @@ +"""Jinja plugins.""" + +import re +import sys + + +BACKTICK_RE = re.compile(r'`(.+?)`') +def backtick(text): + """Regex replacement reordered.""" + return BACKTICK_RE.sub(r"<code>\1</code>", text) + + +def debug(msg): + """Print debugging message during template expansion.""" + print(msg, file=sys.stderr) + + +# If being loaded by MkDocs, register the filters. +if "define_env" in globals(): + def define_env(env): + env.filters["backtick"] = backtick + env.filters["debug"] = debug diff --git a/bin/pretty-html.py b/bin/pretty-html.py new file mode 100644 index 00000000000..a375ec6cad4 --- /dev/null +++ b/bin/pretty-html.py @@ -0,0 +1,6 @@ +"""Prettify HTML.""" + +from bs4 import BeautifulSoup +import sys + +sys.stdout.write(BeautifulSoup(sys.stdin.read(), "html.parser").prettify()) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000000..61c9c4686e7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,18 @@ +site_name: Plotly Libraries +site_description: Documentation for Plotly libraries +site_url: https://example.com +copyright: Plotly Inc. + +docs_dir: ref_pages +site_dir: docs +theme: + name: null + custom_dir: theme + +plugins: + - exclude: + regex: + - '.*\.jinja' + - mkdocs-data-loader: + dir: dist + key: data diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000000..9e8403cb32f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +beautifulsoup4 +html5validator +jinja2 +mkdocs +mkdocs-exclude +python-frontmatter +ruff diff --git a/src/components/errorbars/attributes.js b/src/components/errorbars/attributes.js index d564fe00562..bd57eb82e40 100644 --- a/src/components/errorbars/attributes.js +++ b/src/components/errorbars/attributes.js @@ -16,7 +16,7 @@ module.exports = { description: [ 'Determines the rule used to generate the error bars.', - 'If *constant`, the bar lengths are of a constant value.', + 'If *constant*, the bar lengths are of a constant value.', 'Set this constant in `value`.', 'If *percent*, the bar lengths correspond to a percentage of', diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 32afc99457a..4857a26acba 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -334,7 +334,7 @@ module.exports = { description: [ 'If *normal*, the range is computed in relation to the extrema', 'of the input data.', - 'If *tozero*`, the range extends to 0,', + 'If *tozero*, the range extends to 0,', 'regardless of the input data', 'If *nonnegative*, the range is non-negative,', 'regardless of the input data.', diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index 052749af701..66be8789586 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -73,7 +73,7 @@ var radialAxisAttrs = { dflt: 'tozero', editType: 'calc', description: [ - 'If *tozero*`, the range extends to 0,', + 'If *tozero*, the range extends to 0,', 'regardless of the input data', 'If *nonnegative*, the range is non-negative,', 'regardless of the input data.', diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 688f1a11a50..dc84fba4c32 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -86,7 +86,7 @@ module.exports = { description: [ 'If *normal*, the range is computed in relation to the extrema', 'of the input data.', - 'If *tozero*`, the range extends to 0,', + 'If *tozero*, the range extends to 0,', 'regardless of the input data', 'If *nonnegative*, the range is non-negative,', 'regardless of the input data.' diff --git a/theme/attribute.jinja b/theme/attribute.jinja new file mode 100644 index 00000000000..06e27c0ccb4 --- /dev/null +++ b/theme/attribute.jinja @@ -0,0 +1,7 @@ +{% extends "main.jinja" %} +{% block content %} +<h2>JavaScript Figure Reference: <code>{{page.full_name}}</code></h2> +{% with toplevel=true, parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=details %} + {% include "block.jinja" %} +{% endwith %} +{% endblock %} diff --git a/theme/block.jinja b/theme/block.jinja new file mode 100644 index 00000000000..a133124b5d1 --- /dev/null +++ b/theme/block.jinja @@ -0,0 +1,168 @@ +{# + Document a chunk of the API by iterating through plot schema JSON. + Note: this template may call itself recursively for structured objects. + - toplevel (bool): is this a top-level documentation entry? + - attribute (JSON): data to expand as documentation + - keys_to_ignore (set): keys that are *not* to be documented + - parentlink: slug for parent of this entry + - block: section or "nested" for nested invocations + - parentpath: possibly redundant? + - attribute: dictionary with details to document + - page: information pulled from YAML header of page + - name: name of top-level entry (e.g., "annotations") + - full_name (unused): qualified name (e.g., "layout.annotations") + - description (unused): one-sentence description of this item + - permalink (unused): path to output page +#} +{% set id=[parentlink, "-", page.name] | join %} +{% if toplevel %}<a class="attribute-name" id="{{id}}" href="#{{parentlink}}-{{page.name}}">{{page.name}}</a>{% endif %} +<br> +<em>Parent:</em> <code>{{parentpath | replace('-', '.')}}</code> +<ul> +{% for key, obj in attribute.items() %} + {% if key not in keys_to_ignore %} + <li><code>{{key}}</code> + {% if obj is string %} + {{obj}}{# FIXME: backtick #} + {% elif obj.valType %} + <br> + {% if obj.valType == "enumerated" or obj.valType.values %} + <em>Type:</em> + {{ obj.valType }} + {% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + , one of ( + {% for value in obj["values"] %} + {% if value != false and value != true %}<code>"{{value}}"</code>{% else %}<code>{{value}}</code>{% endif %} + {% if not loop.last %}|{% endif %} + {% endfor %} + ) + + {% elif obj.valType == "number" or obj.valType == "integer" %} + {% if obj["min"] and obj["max"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} between or equal to {{obj["min"]}} and {{obj["max"]}} + {% elif obj["min"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} greater than or equal to {{obj["min"]}} + {% elif obj["max"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} less than or equal to {{obj["min"]}} + {% else %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + {% endif %} + + {% elif obj.valType == "boolean" %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + + {% elif obj.valType == "flaglist" %} + <em>Type:</em> {{ obj.valType }} string. + + Any combination of + {% for value in obj["flags"] %} + {% if value != false and value != true %} + <code>"{{value}}"</code> + {% else %} + <code>{{value}}</code> + {% endif %} + {% if not loop.last %}, {% endif %} + {% endfor %} + joined with a <code>"+"</code> + + {% if obj["extras"] %} + OR + {% for value in obj["extras"] %} + {% if value != false and value != true %} + <code>"{{value}}"</code> + {% else %} + <code>{{value}}</code> + {% endif %} + {% if not loop.last %} or {% endif %} + {% endfor %}. + {% endif %} + + <br> + <em>Examples:</em> + <code>"{{obj["flags"][0]}}"</code>, + <code>"{{obj["flags"][1]}}"</code>, + <code>"{{obj["flags"][0]}}+{{obj["flags"][1]}}"</code> + {% if obj["flags"][2] %}, <code>"{{obj["flags"][0]}}+{{obj["flags"][1]}}+{{obj["flags"][2]}}"</code>{% endif %} + {% if obj["extras"] %}, <code>"{{obj["extras"][0]}}"</code>{% endif %} + + {% elif obj.valType == "data_array" %} + <em>Type:</em> {{obj.valType}} + + {% elif obj.valType == "info_array" %} + <em>Type:</em> {array} + + {% elif obj.valType == "color" %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + + {% elif obj.valType == "any" %} + <em>Type:</em> number or categorical coordinate string + + {% elif obj.valType == "string" %} + <em>Type:</em> string{% if obj["arrayOk"] %} or array of strings{% endif %} + + {% else %} + <em>Type:</em> {{ obj.valType }} + {% endif %} + + {% if obj["role"] == "object" %} + {% if obj["items"] %} + <em>Type:</em> array of objects + {% else %} + <em>Type:</em> object + {% endif %} + {% endif %} + {% endif %} + + {% if obj["dflt"] %} + {% if obj["valType"] == "flaglist" %} + <br><em>Default:</em> <code>"{{ obj["dflt"] }}"</code> + {% else %} + <br><em>Default:</em> + <code> + {%- if obj["dflt"] == "" -%} + "" + {%- elif obj["valType"] == "colorscale" -%} + [{% for d in obj["dflt"] %}[{{d | join(", ")}}], {% endfor %}] + {%- elif obj["valType"] == "info_array" or obj["valType"] == "colorlist" -%} + [{{obj["dflt"] | join(", ")}}] + {%- elif obj["valType"] == "string" or obj["valType"] == "color" or obj["dflt"] == "auto" -%} + "{{ obj["dflt"] }}" + {%- elif obj["valType"] == "enumerated" and obj["dflt"] != true and obj["dflt"] != false -%} + "{{ obj["dflt"] }}" + {%- else -%} + {{obj["dflt"]}} + {%- endif %} + </code> + {% endif %} + {% endif %} + + {% if obj["items"] and obj["valType"] != "info_array" %} + + <br><em>Type:</em> array of object where + each object has one or more of the keys listed below. + {% if page.name == "annotations" %} + {% if not obj["description"] %} + <br>An annotation is a text element that can be placed anywhere in the plot. It can be positioned with respect to relative coordinates in the plot or with respect to the actual data coordinates of the graph. Annotations can be shown with or without an arrow. + {% endif %} + {% endif %} + {% elif obj["role"] == "object" %} + <br><em>Type:</em> object containing one or more of the keys listed below. + {% endif %} + + {% if obj["description"] and obj["description"]!= "" %} + <br> + {{ obj["description"] | replace("*", '"') | escape}}{# FIXME: backtick #} + {% endif %} + + {% if obj["role"] == "object" %} + {% set localparentlink=[parentlink, "-", page.name] | join %} + {% set localparentpath=[parentpath, "-", page.name] | join %} + {% with toplevel=False, parentlink=localparentlink, block="nested", parentpath=localparentpath, attribute=obj %} + {% include "block.jinja" %} + {% endwith %} + {% endif %} + + </li> + {% endif %} +{% endfor %} +</ul> diff --git a/theme/global.jinja b/theme/global.jinja new file mode 100644 index 00000000000..facb3d30d3b --- /dev/null +++ b/theme/global.jinja @@ -0,0 +1,14 @@ +{% extends "main.jinja" %} +{% block content %} +<h2>JavaScript Figure Reference: <code>layout</code></h2> +{% with parentlink=page.layout, block=page.layout, parentpath=page.layout, mustmatch=page.global, attribute=config["data"]["plot-schema"]["layout"]["layoutAttributes"] %} + {% include "block.jinja" %} +{% endwith %} +{% for trace in config["data"]["plot-schema"]["traces"] %} + {% if trace[1].layoutAttributes %} + {% with parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=trace[1].layoutAttributes %} + {% include "block.jinja" %} + {% endwith %} + {% endif %} +{% endfor %} +{% endblock %} diff --git a/theme/main.jinja b/theme/main.jinja new file mode 100644 index 00000000000..f4485eeb2ff --- /dev/null +++ b/theme/main.jinja @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> + <head> + {% block head_title %}<title>{{page["title"]}}</title>{% endblock %} + {% block head_includes %}{% endblock %} + </head> + <body> + <main> + {% block page_title %}<h1>{{page["title"]}}</h1>{% endblock %} + + {# reach into the schema data and pull out details #} + {% if "full_name" not in page.meta %} + {# trace #} + {% set details=page.meta %} + {% else %} + {% set temp_key=page.meta.name.split(".")[-1] %} + {% set temp=config["data"]["plot-schema"]["layout"]["layoutAttributes"][temp_key] %} + {% if "_isSubplotObj" in temp %} + {# subplot #} + {% set details=temp %} + {% else %} + {# everything that isn't trace or subplot #} + {% set details=temp["items"].values() | first %} + {% endif %} + {% endif %} + + {% block content %}{% endblock %} + </main> + </body> +</html> diff --git a/theme/trace.jinja b/theme/trace.jinja new file mode 100644 index 00000000000..788e0c95367 --- /dev/null +++ b/theme/trace.jinja @@ -0,0 +1,22 @@ +{% extends "main.jinja" %} +{% block content %} + +<h2>JavaScript Figure Reference: <code>page.trace</code> Traces</h2> +{% with trace_name=page.meta.trace, trace_data=config["data"]["plot-schema"].traces[page.meta.trace] %} + <div class="description"> + A <code>{{trace_name}}</code> trace is an object with the key <code>"type"</code> equal to <code>"{{trace_data.attributes.type}}"</code> + (i.e. <code>{"type": "{{trace_data.attributes.type}}"}</code>) and any of the keys listed below. + <br> + {{trace_data.meta.description}}{# FIXME: backtick #} + </div> + + {% set localparentlink=trace_name %} + {% set localparentpath="FIXME" %} + {% set attribute=trace_data.attributes %} + {% with parentlink=localparentlink, block="data", parentpath=localparentpath %} + {% include "block.jinja" %} + {% endwith %} + +{% endwith %} +{% endblock %} + From 86f5f55480da6fed94676cddb27ec8b0ffae6780 Mon Sep 17 00:00:00 2001 From: Greg Wilson <greg.wilson@plot.ly> Date: Mon, 9 Dec 2024 13:15:02 -0500 Subject: [PATCH 2/4] feat: modify to work with mkdocs-material as well 1. Move configuration `mkdocs.yml` to `mkdocs-vanilla.yml`. 1. Move logic to set `details` from `theme/main.jinja` to `theme/attribute.jinja` where it is actually used. 1. Create new `mkdocs-material.yml` configuration file with mkdocs-material settings. 1. Change configuration to load overrides from `overrides` directory. 1. Copy template files *without* `main.jinja` to `overrides` directory. 1. Modify `Makefile` to build `mkdocs-vanilla` (our own) or `mkdocs-material`. --- Makefile | 10 +- mkdocs-material.yml | 18 ++++ mkdocs.yml => mkdocs-vanilla.yml | 0 overrides/attribute.jinja | 23 +++++ overrides/block.jinja | 168 +++++++++++++++++++++++++++++++ overrides/global.jinja | 16 +++ overrides/trace.jinja | 23 +++++ requirements.txt | 1 + theme/attribute.jinja | 26 ++++- theme/main.jinja | 17 ---- 10 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 mkdocs-material.yml rename mkdocs.yml => mkdocs-vanilla.yml (100%) create mode 100644 overrides/attribute.jinja create mode 100644 overrides/block.jinja create mode 100644 overrides/global.jinja create mode 100644 overrides/trace.jinja diff --git a/Makefile b/Makefile index eca2626bb5b..ffcf2fe2cdd 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,13 @@ SCHEMA=test/plot-schema.json REF_PAGES=ref_pages THEME=theme -## site: rebuild with MkDocs -site: - mkdocs build +## material: rebuild with MkDocs using mkdocs-material and 'overrides' directory +material: + mkdocs build -f mkdocs-material.yml + +## vanilla: rebuild with MkDocs using 'theme' directory (our own) +vanilla: + mkdocs build -f mkdocs-vanilla.yml ## pages: make all the pages pages: diff --git a/mkdocs-material.yml b/mkdocs-material.yml new file mode 100644 index 00000000000..af480177fbd --- /dev/null +++ b/mkdocs-material.yml @@ -0,0 +1,18 @@ +site_name: Plotly Libraries +site_description: Documentation for Plotly libraries +site_url: https://example.com +copyright: Plotly Inc. + +docs_dir: ref_pages +site_dir: docs +theme: + name: material + custom_dir: overrides + +plugins: + - exclude: + regex: + - '.*\.jinja' + - mkdocs-data-loader: + dir: dist + key: data diff --git a/mkdocs.yml b/mkdocs-vanilla.yml similarity index 100% rename from mkdocs.yml rename to mkdocs-vanilla.yml diff --git a/overrides/attribute.jinja b/overrides/attribute.jinja new file mode 100644 index 00000000000..d74951f21f1 --- /dev/null +++ b/overrides/attribute.jinja @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block content %} + {{ super() }} + {# reach into the schema data and pull out details #} + {% if "full_name" not in page.meta %} + {# trace #} + {% set details=page.meta %} + {% else %} + {% set temp_key=page.meta.name.split(".")[-1] %} + {% set temp=config["data"]["plot-schema"]["layout"]["layoutAttributes"][temp_key] %} + {% if "_isSubplotObj" in temp %} + {# subplot #} + {% set details=temp %} + {% else %} + {# everything that isn't trace or subplot #} + {% set details=temp["items"].values() | first %} + {% endif %} + {% endif %} + <h2>JavaScript Figure Reference: <code>{{page.full_name}}</code></h2> + {% with toplevel=true, parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=details %} + {% include "block.jinja" %} + {% endwith %} +{% endblock %} diff --git a/overrides/block.jinja b/overrides/block.jinja new file mode 100644 index 00000000000..3ef65aec154 --- /dev/null +++ b/overrides/block.jinja @@ -0,0 +1,168 @@ +{# + Document a chunk of the API by iterating through plot schema JSON. + Note: this template may call itself recursively for structured objects. + - toplevel (bool): is this a top-level documentation entry? + - attribute (JSON): data to expand as documentation + - keys_to_ignore (set): keys that are *not* to be documented + - parentlink: slug for parent of this entry + - block: section or "nested" for nested invocations + - parentpath: possibly redundant? + - attribute: dictionary with details to document + - page: information pulled from YAML header of page + - name: name of top-level entry (e.g., "annotations") + - full_name (unused): qualified name (e.g., "layout.annotations") + - description (unused): one-sentence description of this item + - permalink (unused): path to output page +#} +{% set id=[parentlink, "-", page.name] | join %} +{% if toplevel %}<a class="attribute-name" id="{{id}}" href="#{{parentlink}}-{{page.name}}">{{page.name}}</a>{% endif %} +<br> +<em>Parent:</em> <code>{{parentpath | replace('-', '.')}}</code> +<ul> +{% for key, obj in attribute.items() %} + {% if key not in keys_to_ignore %} + <li><code>{{key}}</code> + {% if obj is string %} + {{obj}}<!-- FIXME: backtick --> + {% elif obj.valType %} + <br> + {% if obj.valType == "enumerated" or obj.valType.values %} + <em>Type:</em> + {{ obj.valType }} + {% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + , one of ( + {% for value in obj["values"] %} + {% if value != false and value != true %}<code>"{{value}}"</code>{% else %}<code>{{value}}</code>{% endif %} + {% if not loop.last %}|{% endif %} + {% endfor %} + ) + + {% elif obj.valType == "number" or obj.valType == "integer" %} + {% if obj["min"] and obj["max"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} between or equal to {{obj["min"]}} and {{obj["max"]}} + {% elif obj["min"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} greater than or equal to {{obj["min"]}} + {% elif obj["max"] %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} less than or equal to {{obj["min"]}} + {% else %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + {% endif %} + + {% elif obj.valType == "boolean" %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + + {% elif obj.valType == "flaglist" %} + <em>Type:</em> {{ obj.valType }} string. + + Any combination of + {% for value in obj["flags"] %} + {% if value != false and value != true %} + <code>"{{value}}"</code> + {% else %} + <code>{{value}}</code> + {% endif %} + {% if not loop.last %}, {% endif %} + {% endfor %} + joined with a <code>"+"</code> + + {% if obj["extras"] %} + OR + {% for value in obj["extras"] %} + {% if value != false and value != true %} + <code>"{{value}}"</code> + {% else %} + <code>{{value}}</code> + {% endif %} + {% if not loop.last %} or {% endif %} + {% endfor %}. + {% endif %} + + <br> + <em>Examples:</em> + <code>"{{obj["flags"][0]}}"</code>, + <code>"{{obj["flags"][1]}}"</code>, + <code>"{{obj["flags"][0]}}+{{obj["flags"][1]}}"</code> + {% if obj["flags"][2] %}, <code>"{{obj["flags"][0]}}+{{obj["flags"][1]}}+{{obj["flags"][2]}}"</code>{% endif %} + {% if obj["extras"] %}, <code>"{{obj["extras"][0]}}"</code>{% endif %} + + {% elif obj.valType == "data_array" %} + <em>Type:</em> {{obj.valType}} + + {% elif obj.valType == "info_array" %} + <em>Type:</em> {array} + + {% elif obj.valType == "color" %} + <em>Type:</em> {{ obj.valType }}{% if obj["arrayOk"] %} or array of {{ obj.valType }}s{% endif %} + + {% elif obj.valType == "any" %} + <em>Type:</em> number or categorical coordinate string + + {% elif obj.valType == "string" %} + <em>Type:</em> string{% if obj["arrayOk"] %} or array of strings{% endif %} + + {% else %} + <em>Type:</em> {{ obj.valType }} + {% endif %} + + {% if obj["role"] == "object" %} + {% if obj["items"] %} + <em>Type:</em> array of objects + {% else %} + <em>Type:</em> object + {% endif %} + {% endif %} + {% endif %} + + {% if obj["dflt"] %} + {% if obj["valType"] == "flaglist" %} + <br><em>Default:</em> <code>"{{ obj["dflt"] }}"</code> + {% else %} + <br><em>Default:</em> + <code> + {%- if obj["dflt"] == "" -%} + "" + {%- elif obj["valType"] == "colorscale" -%} + [{% for d in obj["dflt"] %}[{{d | join(", ")}}], {% endfor %}] + {%- elif obj["valType"] == "info_array" or obj["valType"] == "colorlist" -%} + [{{obj["dflt"] | join(", ")}}] + {%- elif obj["valType"] == "string" or obj["valType"] == "color" or obj["dflt"] == "auto" -%} + "{{ obj["dflt"] }}" + {%- elif obj["valType"] == "enumerated" and obj["dflt"] != true and obj["dflt"] != false -%} + "{{ obj["dflt"] }}" + {%- else -%} + {{obj["dflt"]}} + {%- endif %} + </code> + {% endif %} + {% endif %} + + {% if obj["items"] and obj["valType"] != "info_array" %} + + <br><em>Type:</em> array of object where + each object has one or more of the keys listed below. + {% if page.name == "annotations" %} + {% if not obj["description"] %} + <br>An annotation is a text element that can be placed anywhere in the plot. It can be positioned with respect to relative coordinates in the plot or with respect to the actual data coordinates of the graph. Annotations can be shown with or without an arrow. + {% endif %} + {% endif %} + {% elif obj["role"] == "object" %} + <br><em>Type:</em> object containing one or more of the keys listed below. + {% endif %} + + {% if obj["description"] and obj["description"]!= "" %} + <br> + {{ obj["description"] | replace("*", '"') | escape}}<!-- FIXME: backtick --> + {% endif %} + + {% if obj["role"] == "object" %} + {% set localparentlink=[parentlink, "-", page.name] | join %} + {% set localparentpath=[parentpath, "-", page.name] | join %} + {% with toplevel=False, parentlink=localparentlink, block="nested", parentpath=localparentpath, attribute=obj %} + {% include "block.jinja" %} + {% endwith %} + {% endif %} + + </li> + {% endif %} +{% endfor %} +</ul> diff --git a/overrides/global.jinja b/overrides/global.jinja new file mode 100644 index 00000000000..ce30284e180 --- /dev/null +++ b/overrides/global.jinja @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block content %} +{{ super() }} + +<h2>JavaScript Figure Reference: <code>layout</code></h2> +{% with parentlink=page.layout, block=page.layout, parentpath=page.layout, mustmatch=page.global, attribute=config["data"]["plot-schema"]["layout"]["layoutAttributes"] %} + {% include "block.jinja" %} +{% endwith %} +{% for trace in config["data"]["plot-schema"]["traces"] %} + {% if trace[1].layoutAttributes %} + {% with parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=trace[1].layoutAttributes %} + {% include "block.jinja" %} + {% endwith %} + {% endif %} +{% endfor %} +{% endblock %} diff --git a/overrides/trace.jinja b/overrides/trace.jinja new file mode 100644 index 00000000000..b50b0c29c4d --- /dev/null +++ b/overrides/trace.jinja @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block content %} +{{ super() }} + +<h2>JavaScript Figure Reference: <code>page.trace</code> Traces</h2> +{% with trace_name=page.meta.trace, trace_data=config["data"]["plot-schema"].traces[page.meta.trace] %} + <div class="description"> + A <code>{{trace_name}}</code> trace is an object with the key <code>"type"</code> equal to <code>"{{trace_data.attributes.type}}"</code> + (i.e. <code>{"type": "{{trace_data.attributes.type}}"}</code>) and any of the keys listed below. + <br> + {{trace_data.meta.description}}{# FIXME: backtick #} + </div> + + {% set localparentlink=trace_name %} + {% set localparentpath="FIXME" %} + {% set attribute=trace_data.attributes %} + {% with parentlink=localparentlink, block="data", parentpath=localparentpath %} + {% include "block.jinja" %} + {% endwith %} + +{% endwith %} +{% endblock %} + diff --git a/requirements.txt b/requirements.txt index 9e8403cb32f..a15e73779b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ html5validator jinja2 mkdocs mkdocs-exclude +mkdocs-material python-frontmatter ruff diff --git a/theme/attribute.jinja b/theme/attribute.jinja index 06e27c0ccb4..08e703089c0 100644 --- a/theme/attribute.jinja +++ b/theme/attribute.jinja @@ -1,7 +1,25 @@ {% extends "main.jinja" %} {% block content %} -<h2>JavaScript Figure Reference: <code>{{page.full_name}}</code></h2> -{% with toplevel=true, parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=details %} - {% include "block.jinja" %} -{% endwith %} + + {# reach into the schema data and pull out details #} + {% if "full_name" not in page.meta %} + {# trace #} + {% set details=page.meta %} + {% else %} + {% set temp_key=page.meta.name.split(".")[-1] %} + {% set temp=config["data"]["plot-schema"]["layout"]["layoutAttributes"][temp_key] %} + {% if "_isSubplotObj" in temp %} + {# subplot #} + {% set details=temp %} + {% else %} + {# everything that isn't trace or subplot #} + {% set details=temp["items"].values() | first %} + {% endif %} + {% endif %} + + <h2>JavaScript Figure Reference: <code>{{page.full_name}}</code></h2> + {% with toplevel=true, parentlink=page.layout, block=page.layout, parentpath=page.layout, attribute=details %} + {% include "block.jinja" %} + {% endwith %} + {% endblock %} diff --git a/theme/main.jinja b/theme/main.jinja index f4485eeb2ff..604025ee15f 100644 --- a/theme/main.jinja +++ b/theme/main.jinja @@ -7,23 +7,6 @@ <body> <main> {% block page_title %}<h1>{{page["title"]}}</h1>{% endblock %} - - {# reach into the schema data and pull out details #} - {% if "full_name" not in page.meta %} - {# trace #} - {% set details=page.meta %} - {% else %} - {% set temp_key=page.meta.name.split(".")[-1] %} - {% set temp=config["data"]["plot-schema"]["layout"]["layoutAttributes"][temp_key] %} - {% if "_isSubplotObj" in temp %} - {# subplot #} - {% set details=temp %} - {% else %} - {# everything that isn't trace or subplot #} - {% set details=temp["items"].values() | first %} - {% endif %} - {% endif %} - {% block content %}{% endblock %} </main> </body> From fd14bf63713ac67b4d7aff351247029f0b9ab25b Mon Sep 17 00:00:00 2001 From: Greg Wilson <greg.wilson@plot.ly> Date: Mon, 9 Dec 2024 13:26:43 -0500 Subject: [PATCH 3/4] feat: add some CSS styling for homegrown test pages --- Makefile | 1 + assets/mccole.css | 262 +++++++++++++++++++++++++++++++++++++++++++++ mkdocs-vanilla.yml | 2 + theme/main.jinja | 4 +- 4 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 assets/mccole.css diff --git a/Makefile b/Makefile index ffcf2fe2cdd..2b1c5a22ab7 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,7 @@ stubs: ${SCHEMA} --pages ${REF_PAGES} \ --schema ${SCHEMA} \ --verbose + cp assets/*.css ${REF_PAGES} ## validate: check the generated HTML validate: diff --git a/assets/mccole.css b/assets/mccole.css new file mode 100644 index 00000000000..3945558f810 --- /dev/null +++ b/assets/mccole.css @@ -0,0 +1,262 @@ +/* Style variables */ +:root { + --border-thin: solid 1px; + --border-medium: solid 3px; + --color-border: gray; + --color-background: white; + --color-background-code: whitesmoke; + --color-link: #000060; + --color-text: black; + + --expand: 120%; + + --height-half-line: 0.75ex; + + --width-li-adjust: 1rem; + --width-ol-adjust: 0.5rem; + --width-ul-adjust: 0.2rem; + --width-padding: 5px; + --width-page: 72rem; + --width-page-margin: 1rem; + + --stamp-blue-dark: #1B2A83; + --stamp-blue-light: #BABDD8; + --stamp-brown-dark: #5F483C; + --stamp-brown-light: #CEC7C3; + --stamp-green-dark: #7F9971; + --stamp-green-light: #A7E0A3; + --stamp-orange-dark: #AD7353; + --stamp-orange-light: #E5D4CB; + --stamp-purple-dark: #7D6E87; + --stamp-purple-light: #D6D2DA; + --stamp-red-dark: #8B000F; + --stamp-red-light: #DAB3B7; +} + +/* Generic coloring */ +.shaded { + background-color: var(--color-background-code); +} + +/* Generic text alignment */ +.left { text-align: left; } +.center { text-align: center; } +.right { text-align: right; } + +/* Flex grid */ +.row { + display: flex; + flex-flow: row wrap; + width: 100% +} +.row > * { + flex: 1; /* allow children to grow when space available */ +} + +.col-1 { flex-basis: 8.33%; } +.col-2 { flex-basis: 16.66%; } +.col-3 { flex-basis: 25%; } +.col-4 { flex-basis: 33.33%; } +.col-5 { flex-basis: 41.66%; } +.col-6 { flex-basis: 50%; } +.col-7 { flex-basis: 58.33%; } +.col-8 { flex-basis: 66.66%; } +.col-9 { flex-basis: 75%; } +.col-10 { flex-basis: 83.33%; } +.col-11 { flex-basis: 91.66%; } +.col-12 { flex-basis: 100%; } + +/* Hyperlinks */ +a { + color: var(--color-link); +} + +/* Block quotes */ +blockquote { + border: var(--border-medium) var(--color-border); + padding-left: var(--width-page-margin); + padding-right: var(--width-page-margin); +} + +/* Page body */ +body { + background-color: var(--color-background); + color: var(--color-text); + font-family: sans-serif; + font-size: var(--expand); + line-height: var(--expand); + margin: var(--width-page-margin); + max-width: var(--width-page); +} + +/* Code snippets */ +code { + background-color: var(--color-background-code); +} + +/* Definitions in definition lists */ +dd { + margin-bottom: var(--height-half-line); +} +dd:last-of-type { + margin-bottom: 0px; +} + +/* Figures */ +figure { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +figure img { + max-width: 100%; + height: auto; +} + +figcaption { + margin-top: var(--height-half-line); +} + +/* Footer */ +footer { + border-top: var(--border-thin) var(--color-border); + margin-top: var(--height-half-line); + padding-top: var(--height-half-line); + text-align: center; +} + +/* Level-1 heading */ +h1 { + text-align: center; +} + +/* Other headings */ +h2, h3, h4, h5, h6 { + font-style: italic; +} + +/* Navigation */ +nav { + background-color: var(--color-background-code); +} + +/* Ordered lists */ +ol { + margin-left: var(--width-ol-adjust); + padding-left: var(--width-ol-adjust); +} +ol li { + margin-left: var(--width-li-adjust); +} +ol.appendices { + list-style-type: upper-alpha; +} +ol.chapters { +} + +/* Subtitle */ +p.subtitle { + font-weight: bold; + font-style: italic; + text-align: center; +} + +/* Code blocks */ +pre { + border: var(--border-thin) var(--color-border); + padding: var(--width-padding); + background-color: var(--color-background-code); +} + +/* Generic output */ +pre[class*='language'] { + border-left: var(--border-medium); + border-top: var(--border-thin); + border-bottom: var(--border-thin); + border-right: 0px; + padding-left: var(--width-padding); +} + +/* Data files */ +pre.language-csv, +pre.language-json, +pre.language-md, +pre.language-toml, +pre.language-yml { + border-color: var(--stamp-orange-light); +} + +/* JavaScript */ +pre.language-js { + border-color: var(--stamp-blue-light); +} + +/* Output */ +pre.language-out { + border-color: var(--stamp-brown-light); + font-style: italic; +} + +/* Python */ +pre.language-py { + border-color: var(--stamp-blue-light); +} + +/* Shell */ +pre.language-sh { + border-color: var(--stamp-green-light); +} + +/* SQL */ +pre.language-sql { + border-color: var(--stamp-red-light); +} + +/* Transcripts */ +pre.language-text { + border-color: var(--stamp-purple-light); +} + + +/* Tables */ +table { + border-collapse: collapse; + caption-side: bottom; + margin-left: auto; + margin-right: auto; +} +th, td { + padding: var(--width-padding); + vertical-align: top; + border: var(--border-thin); + min-width: 8rem; +} + +/* Unordered lists */ +ul { + list-style-type: disc; + margin-left: var(--width-ul-adjust); + padding-left: var(--width-ul-adjust); +} +ul li { + margin-left: var(--width-li-adjust); +} + +.error-message { + color: red; + font-weight: bold; +} + +/* Dark mode disabled for now */ +/* +@media (prefers-color-scheme: dark) { + :root { + --color-background: #202020; + --color-background-code: #000060; + --color-text: white; + --color-link: lightgray; + } +} +*/ diff --git a/mkdocs-vanilla.yml b/mkdocs-vanilla.yml index 61c9c4686e7..473c07ed7cc 100644 --- a/mkdocs-vanilla.yml +++ b/mkdocs-vanilla.yml @@ -8,6 +8,8 @@ site_dir: docs theme: name: null custom_dir: theme +extra_css: + - mccole.css plugins: - exclude: diff --git a/theme/main.jinja b/theme/main.jinja index 604025ee15f..a677ab3d046 100644 --- a/theme/main.jinja +++ b/theme/main.jinja @@ -2,7 +2,9 @@ <html> <head> {% block head_title %}<title>{{page["title"]}}</title>{% endblock %} - {% block head_includes %}{% endblock %} + {% block head_includes %} + <link href="{{base_url}}/mccole.css" rel="stylesheet" type="text/css"/> + {% endblock %} </head> <body> <main> From b4c4f14673a6066394ed9b69557053cf8134368c Mon Sep 17 00:00:00 2001 From: Greg Wilson <greg.wilson@plot.ly> Date: Mon, 27 Jan 2025 10:47:03 -0500 Subject: [PATCH 4/4] fix: reformat docs in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2b1c5a22ab7..942e053dd3d 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ stubs: ${SCHEMA} validate: @html5validator --root docs -## regenerate JavaScript schema +## schema: regenerate JavaScript schema schema: npm run schema dist