expressive-code / expressive-code

A text marking & annotation engine for presenting source code on the web.

Home Page:https://expressive-code.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

PLugin for popover information on a line

santiemanuel opened this issue · comments

I'm trying to add a button to the code to show extra info about a specific line of code with the following syntax:

git [options] <command> [args] $#$ "Message for the popover box that will show when I click the button"

I've checked the custom plugin example but I think what I want to do is different since it's an element that potentially will get out side of the code block when opened. This is what I got so far:
image

I got two issues in here:

  1. The button doesn't render at the end of the line, but bellow.
  2. If the text has more than 1 line, it will be cut by the code block.

My current implementation is as follows (aided by Claude and EC documentation):

import { ExpressiveCodeAnnotation } from '@expressive-code/core'
import { h } from 'hastscript'

class MessageBoxAnnotation extends ExpressiveCodeAnnotation {
  constructor(options) {
    super(options)
    this.message = options.message
  }

  /** @param {import('@expressive-code/core').AnnotationRenderOptions} context */
  render() {
    return []
  }
}

/** @returns {import('@expressive-code/core').ExpressiveCodePlugin} */
export function pluginMessageBox() {
  return {
    name: 'Message Box',
    baseStyles: `
      .message-box-button {
        appearance: none;
        background: none;
        border: none;
        padding: 0;
        margin-left: 0.5rem;
        cursor: pointer;
        font-size: 1rem;
      }
      
      .message-box-popover {
        display: block;
        position: absolute;
        right: 3.5rem;
        background-color: #fff;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 0.5rem;
        margin-top: 0.5rem;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        z-index: 100;
      }
      
      .message-box-popover.show {
        display: block;
      }
    `,
    hooks: {
      postprocessAnalyzedCode: (context) => {
        if (!context.codeBlock.meta.includes('message-box')) return
        
        context.codeBlock.getLines().forEach((line) => {
          const match = line.text.match(/\$#\$\s*(.+)$/)
          
          if (match) {
            const messageIdx = match.index
            const messageEndIdx = match.index + match[0].length
            const message = match[1]
            
            line.addAnnotation(
              new MessageBoxAnnotation({
                inlineRange: {
                  columnStart: messageIdx,
                  columnEnd: messageEndIdx,
                },
                message,
              })
            )
            
            line.editText(messageIdx, messageEndIdx, '')
          }
        })
      },
      postprocessRenderedLine: (context) => {
        const annotations = context.line.getAnnotations()
        
        annotations.forEach((annotation) => {
          if (annotation instanceof MessageBoxAnnotation) {
            const button = h('button.message-box-button', '📝')
            const popover = h('div.message-box-popover', annotation.message)
            
            button.properties.onclick = () => {
              popover.classList.toggle('show')
            }
            
            context.renderData.lineAst.children.push(button)
            context.renderData.lineAst.children.push(popover)
          }
        })
      },
    },
  }
}

I had to move the elements added to the postProcessRenderedLine because the render method from MessageBoxAnnotation threw an error like the following:
Expected annotation render function to return an array of 0 node(s), but got [{"type":"element","tagName":"button","properties":{"className":["message-box-button"]},"children":[{"type":"text","value":"📝"}]},{"type":"element","tagName":"div","properties":{"className":["message-box-popover"]},"children":[]}].

If there's anything else I could provide please let me know.