All files / Bandstand/src/lib abc-svg.ts

80% Statements 24/30
83.33% Branches 5/6
100% Functions 2/2
80% Lines 24/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 782x                     2x                                                                           2x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x     2x     2x 2x 2x 2x 2x 2x 2x     2x 2x 2x  
import {Worker} from 'worker_threads'
 
/**
 * ABC notation → SVG on the server, for PDFs. abcjs draws into a DOM, so it
 * runs against jsdom — in a short-lived worker thread, because jsdom's
 * `window`/`document` globals must never leak into the server itself (code
 * would start thinking it runs in a browser).
 */
 
export type AbcJob = {abc: string; steps: number}
 
const WORKER = `
const {parentPort, workerData} = require('worker_threads')
const {JSDOM} = require('jsdom')
const dom = new JSDOM('<!doctype html><body></body>', {pretendToBeVisual: true})
global.window = dom.window
global.document = dom.window.document
global.navigator = dom.window.navigator
const ABCJS = require('abcjs')
parentPort.postMessage(
  workerData.map(({abc, steps}) => {
    try {
      const el = document.createElement('div')
      document.body.appendChild(el)
      ABCJS.renderAbc(el, abc, {
        visualTranspose: steps,
        foregroundColor: '#000',
        staffwidth: 420,
        paddingtop: 0, paddingbottom: 0, paddingleft: 0, paddingright: 0,
      })
      const svg = el.querySelector('svg')
      if (!svg) return null
      // svg-to-pdfkit wants plain markup: drop abcjs's embedded <style>
      svg.querySelectorAll('style').forEach((s) => s.remove())
      return {
        svg: svg.outerHTML,
        width: parseFloat(svg.getAttribute('width')) || 420,
        height: parseFloat(svg.getAttribute('height')) || 100,
      }
    } catch (e) {
      return null
    }
  }),
)
`
 
export type AbcSvg = {svg: string; width: number; height: number}
 
/** One SVG per job, in order; null where the notation couldn't be drawn. */
export function renderAbcSvgs(
  jobs: AbcJob[],
  timeoutMs = 15_000,
): Promise<(AbcSvg | null)[]> {
  if (!jobs.length) return Promise.resolve([])
  return new Promise((resolve) => {
    const none = () => jobs.map(() => null)
    let w: Worker
    try {
      w = new Worker(WORKER, {eval: true, workerData: jobs})
    } catch {
      return resolve(none())
    }
    const timer = setTimeout(() => {
      w.terminate()
      resolve(none())
    }, timeoutMs)
    w.once('message', (out) => {
      clearTimeout(timer)
      resolve(out)
      w.terminate()
    })
    w.once('error', () => {
      clearTimeout(timer)
      resolve(none())
    })
  })
}