/** * Annotation Guide Plugin * * Displays variant-specific annotation guidelines in a left-side drawer. * Fetches markdown documentation from configured URLs and renders it. */ /** * @import { PluginContext } from '../modules/plugin-context.js' * @import { ApplicationState } from '../state.js' * @import MarkdownIt from 'markdown-it' * @import { SlDrawer } from '../ui.js' * @import { annotationGuideDrawerPart } from '../templates/annotation-guide-drawer.types.js' */ import { Plugin } from '../modules/plugin-base.js' import { registerTemplate, createSingleFromTemplate } from '../modules/ui-system.js' import { createMarkdownRenderer, fetchMarkdown, renderMarkdown } from '../modules/markdown-utils.js' /** * Annotation guide information from extractor plugins * @typedef {object} AnnotationGuideInfo * @property {string} variant_id - The variant identifier * @property {"html" | "markdown"} type - The content type * @property {string} url - The URL to fetch the guide from */ // Register template await registerTemplate('annotation-guide-drawer', 'annotation-guide-drawer.html') class AnnotationGuidePlugin extends Plugin { /** @param {PluginContext} context */ constructor(context) { super(context, { name: 'annotation-guide', deps: ['help', 'extraction', 'dialog', 'logger'] }) } get #extraction() { return this.getDependency('extraction') } get #client() { return this.getDependency('client') } /** @type {SlDrawer & annotationGuideDrawerPart} */ #ui = null /** @type {MarkdownIt | null} */ #md = null /** @type {AnnotationGuideInfo[]} */ #annotationGuides = [] /** @type {string | null} */ #currentGuideUrl = null /** * @param {ApplicationState} _state */ async install(_state) { await super.install(_state) this.getDependency('logger').debug(`Installing plugin "annotation-guide"`) this.#ui = this.createUi(createSingleFromTemplate('annotation-guide-drawer', document.body)) this.#ui.closeBtn.addEventListener('click', () => this.#ui.hide()) this.#ui.openInNewWindowBtn.addEventListener('click', () => this.#openInNewWindow()) this.getDependency('help').registerTopic( 'Annotation Guide', 'file-text', () => this.open() ) this.#md = createMarkdownRenderer() // @ts-ignore window.appAnnotationGuide = this } /** * Opens the annotation guide drawer */ async open() { this.#ui.show() if (this.#annotationGuides.length === 0) { let extractors = this.#extraction.extractorInfo() if (!extractors) { extractors = await this.#client.getExtractorList() } if (extractors) { this.#annotationGuides = extractors.flatMap(e => e.annotationGuides || []) } } const variant = this.state?.variant if (variant) { await this.load(variant) } else { this.#ui.openInNewWindowBtn.hidden = true this.#ui.content.innerHTML = `

No document loaded.

Load a document to view its annotation guide.

` } } /** * Loads annotation guide for a specific variant * @param {string} variant The variant identifier */ async load(variant) { this.#ui.content.innerHTML = "" const variantGuides = this.#annotationGuides.filter(g => g.variant_id === variant) const markdownGuide = variantGuides.find(g => g.type === 'markdown') const htmlGuide = variantGuides.find(g => g.type === 'html') this.#currentGuideUrl = htmlGuide?.url || null this.#ui.openInNewWindowBtn.hidden = !htmlGuide if (!markdownGuide && !htmlGuide) { this.#ui.content.innerHTML = `

No annotation guide is available for variant: ${variant}

Check back later or contact your administrator for documentation.

` return } if (!markdownGuide && htmlGuide) { this.#ui.content.innerHTML = `

The annotation guide for this variant is available as an external page.

Open Annotation Guide

` return } const guideUrl = /** @type {AnnotationGuideInfo} */(markdownGuide).url const [fetchUrl, anchor] = guideUrl.split('#') try { const logger = this.getDependency('logger') logger.debug(`Loading annotation guide from: ${fetchUrl}`) const markdown = await fetchMarkdown(fetchUrl, true) const html = renderMarkdown(/** @type {MarkdownIt} */(this.#md), markdown, { localLinkHandler: 'appAnnotationGuide.load', openExternalInNewTab: true }) this.#ui.content.innerHTML = html if (anchor) { setTimeout(() => { const targetElement = this.#ui.content.querySelector(`#${anchor}`) if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) } else { logger.warn(`Anchor #${anchor} not found in annotation guide`) } }, 100) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) this.getDependency('logger').error(`Failed to load annotation guide: ${errorMessage}`) this.getDependency('dialog').error(`Failed to load annotation guide: ${errorMessage}`) this.#ui.content.innerHTML = `

Error loading annotation guide

${errorMessage}

` } } /** * Closes the annotation guide drawer */ close() { this.#ui.hide() } #openInNewWindow() { if (this.#currentGuideUrl) { window.open(this.#currentGuideUrl, '_blank') } } } export default AnnotationGuidePlugin export const plugin = AnnotationGuidePlugin