Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code annotations fail to render with techdocs-core plugin enabled #128

Closed
rmartine-ias opened this issue Jun 8, 2023 · 6 comments
Closed

Comments

@rmartine-ias
Copy link

Something techdocs-core is doing (maybe setting theme.palette to ''?) prevents code annotations from rendering.

Reproduction:

python3.11 -m venv .venv
source .venv/bin/activate
pip install mkdocs-techdocs-core
mkdir docs

mkdocs.yml:

site_name: Backstage Docs
theme:
  name: material
  features:
    - content.code.annotate
nav:
  - Home: index.md
markdown_extensions:
  - pymdownx.superfences

docs/index.md:

```yaml
# (1)
```

1. An annotation
mkdocs build
open site/index.html # You should see a clickable annotation
yq -i '.plugins[0] = "techdocs-core"' mkdocs.yml
mkdocs build
open site/index.html # You should not see an annotation

Without techdocs:
Screenshot 2023-06-08 at 3 54 20 PM

With it:
Screenshot 2023-06-08 at 3 54 24 PM

I noticed that if I have no plugins set, and set theme.palette to "", then annotations also fail to render. So I think this might be causing it, but don't see an easy fix.

@steven-terrana-bah
Copy link

steven-terrana-bah commented Jul 28, 2023

I came to the same conclusion that this issue was being caused by theme.palette: "" so i tried creating a hook that overrides this value after the techdocs-core plugin makes its configuration changes (just to see what would happen).

This fixed code annotation rendering on the mkdocs site directly (http://localhost:8000) but not in the techdocs rendering within backstage (http://localhost:3000) when running techdocs-cli serve.

mkdocs.yml

site_name: test-annotations

theme:
  name: material
  features:
    - content.code.annotate 

plugins: 
 - techdocs-core

hooks:
- hooks.py

markdown_extensions:
  - pymdownx.highlight:
      anchor_linenums: true
      line_spans: __span
      pygments_lang_class: true
  - pymdownx.inlinehilite
  - pymdownx.snippets
  - pymdownx.superfences

nav:
  - home: index.md

hook.py

import mkdocs

@mkdocs.plugins.event_priority(-100)
def on_config(config):
    config['theme']['palette'] = {}
    config['theme']['palette']['scheme'] = 'default'
    return config

@github-actions
Copy link

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added the stale label Sep 27, 2023
@rmartine-ias
Copy link
Author

still an issue

@github-actions github-actions bot removed the stale label Oct 3, 2023
@Gholie
Copy link

Gholie commented Oct 3, 2023

We are also looking to use annotations. Related issue #145 for updates on core packages relevant as well

@alexlorenzi
Copy link
Contributor

Hi, @rmartine-ias.
Thanks for spotting this, it certainly does mess up the HTML generated. We have an update going in that will fix this issue.

However, two other issues mean the annotations don't display from within Backstage:

First, we sanitize the HTML rendered by MkDocs in the TechDoc plugin before we display it on the page. This will remove the script tags that are generated by the plugin for the annotations.

Second, even if we modified the sanitization to keep the scripts, the way the rendered DOM is attached to the page still means that the javascript doesn't execute. We use the DomParser to convert the HTML string into DOM elements, and through this, any script tags get marked as unexecutable.

Because of this, we'd require quite a drastic change to how the frontend plugin works to enable this, so it's not something we can support currently.

@vanchaxy
Copy link

It's not perfect but I made it work using addon. Most of the code copied from mkdocs material.

ezgif-2-d103e08132

Code:

import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
import ReactDOM from "react-dom/client";
import React from 'react';

import Popover from '@material-ui/core/Popover';

function findHosts(container: HTMLElement): HTMLElement[] {
  return container.tagName === "CODE"
    ? Array.from(container.querySelectorAll(".c, .c1, .cm"))
    : [container]
}

function findMarkers(container: HTMLElement): Text[] {
  const markers: Text[] = []
  console.log(findHosts(container))
  for (const el of findHosts(container)) {
    const nodes: Text[] = []

    /* Find all text nodes in current element */
    const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT)
    for (let node = it.nextNode(); node; node = it.nextNode())
      nodes.push(node as Text)

    /* Find all markers in each text node */
    for (let text of nodes) {
      let match: RegExpExecArray | null

      /* Split text at marker and add to list */
      while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) {
        const [, id, force] = match
        if (typeof force === "undefined") {
          const marker = text.splitText(match.index)
          text = marker.splitText(id.length)
          markers.push(marker)

        /* Replace entire text with marker */
        } else {
          text.textContent = id
          markers.push(text)
          break
        }
      }
    }
  }
  return markers
}

function findCandidateList(el: HTMLElement): HTMLElement | undefined {
  if (el.nextElementSibling) {
    const sibling = el.nextElementSibling as HTMLElement
    if (sibling.tagName === "OL")
      return sibling

    /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
    else if (sibling.tagName === "P" && !sibling.children.length)
      return findCandidateList(sibling)
  }

  /* Everything else */
  return undefined
}

function getOptionalElement<T extends HTMLElement>(
  selector: string, node: ParentNode = document
): T | undefined {
  return node.querySelector<T>(selector) || undefined
}


function AnnotationPopover({ content }: { content: any }) {
  const [anchor, setAnchor] = React.useState(null);

  const handleClick = (event: any) => {
    setAnchor(event.currentTarget);
  };

  const handleClose = () => {
    setAnchor(null);
  };

  const open = Boolean(anchor);
  const id = open ? 'annotation-popover' : undefined;

  return (
    <>
      <span
          className="md-annotation__index"
          style={{}}
          onClick={handleClick}
      >
        <span data-md-annotation-id=''></span>
      </span>
      <Popover
        id={id}
        open={open}
        anchorEl={anchor}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'center',
          horizontal: 'center',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'left',
        }}
      >
        <div
            style={{'padding': '.8rem', 'maxWidth': 'calc(100vw - 1.6rem)', 'width': '20rem'}}
            dangerouslySetInnerHTML={{ __html: content }}>
        </div>
      </Popover>
    </>
  );
}

export const AnnotationAddon = () => {
  const containers = useShadowRootElements<HTMLImageElement>(['.annotate', 'pre:not(.mermaid) > code']);
  const heads = useShadowRootElements<HTMLImageElement>(['head']);
  const [cssAdded, setCssAdded] = useState<boolean>(false);

  useEffect(() => {
      for (let container of containers) {
        let annotationsList;
        if (container.tagName === 'CODE') {
          const highlight = container.closest('.highlight');
           if (!(highlight instanceof HTMLElement)) return;
          annotationsList = findCandidateList(highlight);
        } else {
          annotationsList = findCandidateList(container);
        }
        if (!annotationsList) return;
        for (const marker of findMarkers(container)) {
          const [, id] = marker.textContent!.match(/\((\d+)\)/)!
          const popup = getOptionalElement(`:scope > li:nth-child(${id})`, annotationsList);
          if (popup) {
            const aside = document.createElement('aside');
            aside.className = 'md-annotation';
            marker.replaceWith(aside);
            const root = ReactDOM.createRoot(aside)
            root.render(<AnnotationPopover content={popup.innerHTML} />);
          }
        }
        annotationsList.remove()
      }
  }, [containers]);

    useEffect(() => {
    if (cssAdded) return;
    const styleElement = document.createElement('style');
    styleElement.innerText = `
    :host {
      --md-annotation-bg-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2Z"/></svg>');
      --md-annotation-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 13h-4v4h-2v-4H7v-2h4V7h2v4h4m-5-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2Z"/></svg>')
    }
    `;

    heads.forEach((head) => {
      head.parentNode?.insertBefore(styleElement, head.nextSibling);
    });
    setCssAdded(true);
  }, [heads, cssAdded]);

  return null;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants