-- Copyright (C) 2006-2025 The Gregorio Project (see CONTRIBUTORS.md)
--
-- This file is part of Gregorio.
--
-- Gregorio is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- Gregorio is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with Gregorio.  If not, see <http://www.gnu.org/licenses/>.

local P = lpeg.P
local R = lpeg.R
local C = lpeg.C

local function sort_keys(table_to_sort, compare)
  local sorted = {}, key
  for key in pairs(table_to_sort) do
    table.insert(sorted, key)
  end
  table.sort(sorted, compare)
  return sorted
end

local function sort_unique_keys(tables, compare)
  local set = {}, ignored, table_to_scan, key
  for ignored, table_to_scan in pairs(tables) do
    if table_to_scan then
      for key in pairs(table_to_scan) do
        set[key] = true
      end
    end
  end
  return sort_keys(set)
end

local EXCLUDE = {
  ['.notdef'] = true,
  ['.null'] = true,
  notdef = true,
  nonmarkingreturn = true,
  AscendensOriscusLineBLTR = true,
  AscendensOriscusLineTR = true,
  BracketLeftZero = true,
  BracketLeftSix = true,
  BracketLeftSeven = true,
  BracketLeftEight = true,
  BracketLeftNine = true,
  BracketLeftTen = true,
  BracketLeftEleven = true,
  BracketLeftTwelve = true,
  BracketLeftThirteen = true,
  BracketLeftFourteen = true,
  BracketLeftShortZero = true,
  BracketLeftShortSix = true,
  BracketLeftShortSeven = true,
  BracketLeftShortEight = true,
  BracketLeftShortNine = true,
  BracketLeftShortTen = true,
  BracketLeftShortEleven = true,
  BracketLeftShortTwelve = true,
  BracketLeftShortThirteen = true,
  BracketLeftShortFourteen = true,
  BracketLeftLongZero = true,
  BracketLeftLongSix = true,
  BracketLeftLongSeven = true,
  BracketLeftLongEight = true,
  BracketLeftLongNine = true,
  BracketLeftLongTen = true,
  BracketLeftLongEleven = true,
  BracketLeftLongTwelve = true,
  BracketLeftLongThirteen = true,
  BracketLeftLongFourteen = true,
  BracketRightZero = true,
  BracketRightSix = true,
  BracketRightSeven = true,
  BracketRightEight = true,
  BracketRightNine = true,
  BracketRightTen = true,
  BracketRightEleven = true,
  BracketRightTwelve = true,
  BracketRightThirteen = true,
  BracketRightFourteen = true,
  BracketRightShortZero = true,
  BracketRightShortSix = true,
  BracketRightShortSeven = true,
  BracketRightShortEight = true,
  BracketRightShortNine = true,
  BracketRightShortTen = true,
  BracketRightShortEleven = true,
  BracketRightShortTwelve = true,
  BracketRightShortThirteen = true,
  BracketRightShortFourteen = true,
  BracketRightLongZero = true,
  BracketRightLongSix = true,
  BracketRightLongSeven = true,
  BracketRightLongEight = true,
  BracketRightLongNine = true,
  BracketRightLongTen = true,
  BracketRightLongEleven = true,
  BracketRightLongTwelve = true,
  BracketRightLongThirteen = true,
  BracketRightLongFourteen = true,
  PunctumAuctusLineBL = true,
  PunctumLineBLBR = true,
  PunctumLineBR = true,
  PunctumLineTR = true,
  PunctumSmall = true,
  FlexusLineBL = true,
  FlexusAmOneLineBL = true,
  DescendensOriscusLineTR = true,
  DescendensOriscusLineBLTR = true,
  QuilismaLineTR = true,
  VirgaLineBR = true,
  SalicusOriscus = true,
  VirgaBaseLineBL = true,
  ['VirgulaTwo'] = true,
  ['VirgulaThree'] = true,
  ['VirgulaFive'] = true,
  ['VirgulaSix'] = true,
  ['VirgulaParenTwo'] = true,
  ['VirgulaParenThree'] = true,
  ['VirgulaParenFive'] = true,
  ['VirgulaParenSix'] = true,
  ['DivisioMinimisTwo'] = true,
  ['DivisioMinimisThree'] = true,
  ['DivisioMinimisFive'] = true,
  ['DivisioMinimisSix'] = true,
  ['DivisioMinimaTwo'] = true,
  ['DivisioMinimaThree'] = true,
  ['DivisioMinimaFive'] = true,
  ['DivisioMinimaSix'] = true,
  ['DivisioMinimaParenTwo'] = true,
  ['DivisioMinimaParenThree'] = true,
  ['DivisioMinimaParenFive'] = true,
  ['DivisioMinimaParenSix'] = true,
  ['DivisioMinorTwo'] = true,
  ['DivisioMinorThree'] = true,
  ['DivisioMinorFive'] = true,
  ['DivisioMaiorTwo'] = true,
  ['DivisioMaiorThree'] = true,
  ['DivisioMaiorFive'] = true,
  ['DivisioMaiorDottedTwo'] = true,
  ['DivisioMaiorDottedThree'] = true,
  ['DivisioMaiorDottedFive'] = true,
  ['DivisioMaiorDottedBackingTwo'] = true,
  ['DivisioMaiorDottedBackingThree'] = true,
  ['DivisioMaiorDottedBackingFive'] = true,
}

-- &&& in the following two tables is a placeholder for the cavum shape 'r'

local GABC = {
  Accentus = [[\excluded{g}r1]],
  AccentusReversus = [[\excluded{g}r2]],
  Ancus = [[g&&&ec]],
  AncusLongqueue = [[h&&&fd]],
  AscendensOriscus = [[g&&&o1]],
  AscendensOriscusLineBL = [[\excluded{e}@g&&&o1]],
  AscendensOriscusLineTL = [[\excluded{i}@g&&&o1]],
  AscendensOriscusScapus = [[g&&&O1]],
  AscendensOriscusScapusLongqueue = [[h&&&O1]],
  AscendensOriscusScapusOpenqueue = [[a&&&O1]],
  AscendensPunctumInclinatum = [[G1&&&]],
  AuctumMora = [[\excluded{g}.]],
  BarBrace = [[\excluded{,}\_]],
  BracketLeft = [=[[[\excluded{ce]]}]=],
  BracketLeftShort = [=[[[\excluded{fh]]}]=],
  BracketLeftLong = [=[[[\excluded{gi]]}]=],
  BracketRight = [=[\excluded{[[ce}]]]=],
  BracketRightShort = [=[\excluded{[[fh}]]]=],
  BracketRightLong = [=[\excluded{[[gi}]]]=],
  CClef = [[c3]],
  CClefChange = [[c3]],
  Circulus = [[\excluded{g}r3]],
  CurlyBrace = '[ocb:1;6mm]',
  CustosDownLong = [[j+]],
  CustosDownMedium = [[m+]],
  CustosDownShort = [[k+]],
  CustosUpLong = [[f+]],
  CustosUpMedium = [[a+]],
  CustosUpShort = [[g+]],
  DescendensOriscus = [[g&&&o0]],
  DescendensOriscusLineBL = [[\excluded{e}@g&&&o0]],
  DescendensOriscusLineTL = [[\excluded{i}@g&&&o0]],
  DescendensOriscusScapus = [[g&&&O0]],
  DescendensOriscusScapusLongqueue = [[h&&&O0]],
  DescendensOriscusScapusOpenqueue = [[a&&&O0]],
  DescendensPunctumInclinatum = [[G0&&&]],
  DivisioDominican = [[,3]],
  DivisioDominicanAlt = [[,4]],
  DivisioMaiorFour = [[:]],
  DivisioMaiorDottedFour = [[:?]],
  DivisioMaiorDottedBackingFour = [[\excluded{:?}]],
  DivisioMinimaFour = [[,]],
  DivisioMinimaParenFour = [[,?]],
  DivisioMinimisFour = [[\textasciicircum{}]],
  DivisioMinorFour = [[;]],
  FClefChange = [[f3]],
  FClef = [[f3]],
  Flat = [[gx]],
  FlatHole = [[\excluded{gx}]],
  FlatParen = [[gx?]],
  FlatParenHole = [[\excluded{gx?}]],
  Flexus = [[g&&&e]],
  FlexusLongqueue = [[h&&&f]],
  FlexusNobar = [[@h&&&f]],
  FlexusOriscus = [[g&&&oe]],
  FlexusOriscusInusitatus = [[g&&&o1e]],
  FlexusOriscusScapus = [[g&&&Oe]],
  FlexusOriscusScapusInusitatus = [[g&&&O1e]],
  FlexusOriscusScapusInusitatusLongqueue = [[h&&&O1f]],
  FlexusOriscusScapusLongqueue = [[h&&&Of]],
  LeadingOriscus = [[g&&&o\excluded{igig}]],
  LeadingPunctum = [[g&&&\excluded{igig}]],
  LeadingQuilisma = [[g&&&w\excluded{igig}]],
  Linea = [[g&&&=]],
  LineaPunctum = [[g&&&R]],
  Natural = [[gy]],
  NaturalHole = [[\excluded{gy}]],
  NaturalParen = [[gy?]],
  NaturalParenHole = [[\excluded{gy?}]],
  OblatusAscendensOriscus = [[g&&&o1]],
  OblatusDescendensOriscus = [[g&&&o0]],
  OblatusFlexusOriscus = [[g&&&oe]],
  OblatusFlexusOriscusInusitatus = [[g&&&o1e]],
  OblatusPesQuassus = [[g&&&oi]],
  OblatusPesQuassusLongqueue = [[h&&&oj]],
  OblatusPesQuassusInusitatus = [[g&&&o0i]],
  OblatusPesQuassusInusitatusLongqueue = [[h&&&o0j]],
  Oriscus = [[g&&&o]], -- for Deminutus
  Pes = [[g&&&i]],
  PesAscendensOriscus = [[g&&&iO\excluded{/j}]],
  PesDescendensOriscus = [[g&&&iO\excluded{/h}]],
  PesQuadratum = [[g&&&qi]],
  PesQuadratumLongqueue = [[h&&&qj]],
  PesQuassus = [[g&&&oi]],
  PesQuassusInusitatus = [[g&&&o0i]],
  PesQuassusInusitatusLongqueue = [[h&&&o0j]],
  PesQuassusLongqueue = [[h&&&oj]],
  PorrectusFlexus = [[g&&&ege]],
  PorrectusFlexusNobar = [[\excluded{e}g&&&ege]],
  Porrectus = [[g&&&eg]],
  PorrectusLongqueue = [[h&&&fh]],
  PorrectusNobar = [[@g&&&eg]],
  Punctum = [[g&&&]],
  PunctumInclinatum = [[G&&&]], -- for deminutus
  PunctumInclinatumAuctus = [[G&&&>]],
  PunctumLineBL = [[\excluded{e}@g&&&]],
  PunctumLineTL = [[\excluded{i}@g&&&]],
  Quilisma = [[g&&&w]],
  QuilismaPes = [[g&&&wi]],
  QuilismaPesQuadratum = [[g&&&Wi]],
  QuilismaPesQuadratumLongqueue = [[h&&&Wj]],
  RoundBraceDown = '[ub:1;6mm]',
  RoundBrace = '[ob:1;6mm]',
  SalicusFlexus = [[g&&&iOki]],
  Salicus = [[g&&&iOk]],
  SalicusLongqueue = [[h&&&jOl]],
  Scandicus = [[g&&&ik]],
  Semicirculus = [[\excluded{g}r4]],
  SemicirculusReversus = [[\excluded{g}r5]],
  Sharp = [[g\#{}]],
  SharpHole = [[\excluded{g\#{}}]],
  SharpParen = [[g\#{}?]],
  SharpParenHole = [[\excluded{g\#{}?}]],
  StansPunctumInclinatum = [[G2&&&]],
  StrophaAucta = [[g&&&s>]],
  StrophaAuctaLongtail = [[h&&&s>]],
  Stropha = [[g&&&s]],
  Torculus = [[g&&&ig]],
  TorculusLiquescens = [[g&&&ige]],
  TorculusLiquescensQuilisma = [[g&&&wige]],
  TorculusQuilisma = [[g&&&wig]],
  TorculusResupinus = [[g&&&igi]],
  TorculusResupinusQuilisma = [[g&&&wigi]],
  VEpisema = [[\excluded{g}^^^^0027]],
  Virga = [[g&&&v]],
  VirgaLongqueue = [[h&&&v]],
  VirgaOpenqueue = [[a&&&v]],
  VirgaReversa = [[g&&&V]],
  VirgaReversaLongqueue = [[h&&&V]],
  VirgaReversaOpenqueue = [[a&&&V]],
  VirgulaFour = [[^^^^0060]],
  VirgulaParenFour = [[^^^^0060?]],
}

local GABC_AMBITUS_ONE = {
  PorrectusLongqueue = [[h&&&gh]],
  PorrectusFlexusLongqueue = [[h&&&ghg]],
  FlexusOpenqueue = [[b&&&a]],
  FlexusOriscusScapusOpenqueue = [[b&&&Oa]],
  PesQuadratumOpenqueue = [[a&&&qb]],
  PesQuassusOpenqueue = [[a&&&ob]],
  QuilismaPesQuadratumOpenqueue = [[a&&&Wb]],
  OblatusPesQuassusInusitatusOpenqueue = [[a&&&o0b]],
  OblatusPesQuassusOpenqueue = [[b&&&oc]],
}

-- if the item is a table, the values will replace fuse_head and gabc
local GABC_FUSE = {
  Upper = {
    Punctum = [[\excluded{e}@]],
    AscendensOriscus = [[\excluded{e}@]],
    DescendensOriscus = [[\excluded{e}@]],
    OblatusAscendensOriscus = [[\excluded{f}@]],
    OblatusFlexusOriscusInusitatus = [[\excluded{f}@]],
    OblatusPesQuassus = [[\excluded{f}@]],
    OblatusPesQuassusLongqueue = [[\excluded{g}@]],
    OblatusPesQuassusOpenqueue = [[\excluded{a}@]],
    Pes = [[\excluded{e}@]],
    PesQuadratum = [[\excluded{e}@]],
    PesQuadratumLongqueue = [[\excluded{f}@]],
    PesQuadratumOpenqueue = { [[\excluded{a}@]], [[bqc]] },
    PesQuassus = [[\excluded{e}@]],
    PesQuassusInusitatus = [[\excluded{e}@]],
    PesQuassusInusitatusLongqueue = [[\excluded{f}@]],
    PesQuassusLongqueue = [[\excluded{f}@]],
    PesQuassusOpenqueue = { [[\excluded{a}@]], [[cod]] },
    Flexus = [[\excluded{e}@]],
    FlexusOriscus = [[\excluded{e}@]],
    FlexusOriscusInusitatus = [[\excluded{e}@]],
    Virga = [[\excluded{e}@]],
    VirgaLongqueue = [[\excluded{f}@]],
    VirgaOpenqueue = { [[\excluded{a}@]], [[cv]] },
  },
  Lower = {
    Punctum = [[\excluded{i}@]],
    AscendensOriscus = [[\excluded{i}@]],
    DescendensOriscus = [[\excluded{i}@]],
    OblatusDescendensOriscus = [[\excluded{h}@]],
    OblatusFlexusOriscus = [[\excluded{h}@]],
    OblatusPesQuassusInusitatus = [[\excluded{h}@]],
    OblatusPesQuassusInusitatusLongqueue = [[\excluded{i}@]],
    OblatusPesQuassusInusitatusOpenqueue = [[\excluded{b}@]],
    Pes = [[\excluded{i}@]],
    PesQuadratum = [[\excluded{i}@]],
    PesQuadratumLongqueue = [[\excluded{j}@]],
    PesQuadratumOpenqueue = [[\excluded{b}@]],
    PesQuassus = [[\excluded{i}@]],
    PesQuassusInusitatus = [[\excluded{i}@]],
    PesQuassusInusitatusLongqueue = [[\excluded{j}@]],
    PesQuassusLongqueue = [[\excluded{j}@]],
    PesQuassusOpenqueue = [[\excluded{b}@]],
    Flexus = [[\excluded{i}@]],
    FlexusOriscus = [[\excluded{i}@]],
    FlexusOriscusInusitatus = [[\excluded{i}@]],
  },
  Up = {
    Punctum = [[\excluded{@ij}]],
    AscendensOriscus = [[\excluded{@ij}]],
    AscendensOriscusScapus = [[\excluded{@ij}]],
    AscendensOriscusScapusLongqueue = [[\excluded{@jk}]],
    DescendensOriscus = [[\excluded{@ij}]],
    DescendensOriscusScapus = [[\excluded{@ij}]],
    DescendensOriscusScapusLongqueue = [[\excluded{@jk}]],
    OblatusAscendensOriscus = [[\excluded{@i}]],
    OblatusDescendensOriscus = [[\excluded{@i}]],
    Quilisma = [[\excluded{@ij}]],
    Flexus = [[\excluded{@gi}]],
    FlexusNobar = [[\excluded{@hj}]],
  },
  Down = {
    Punctum = [[\excluded{@eg}]],
    AscendensOriscus = [[\excluded{@eg}]],
    AscendensOriscusScapus = [[\excluded{@eg}]],
    AscendensOriscusScapusLongqueue = [[\excluded{@eg}]],
    DescendensOriscus = [[\excluded{@eg}]],
    DescendensOriscusScapus = [[\excluded{@eg}]],
    DescendensOriscusScapusLongqueue = [[\excluded{@eg}]],
    OblatusAscendensOriscus = [[\excluded{@e}]],
    OblatusDescendensOriscus = [[\excluded{@e}]],
    VirgaReversa = [[\excluded{@eg}]],
    VirgaReversaLongqueue = [[\excluded{@fg}]],
  },
}

local DEBILIS = {
  InitioDebilis = [[-]],
  [''] = [[]],
}

local LIQUESCENCE = {
  Ascendens = [[<]],
  Descendens = [[>]],
  Deminutus = [[\~{}]],
  Nothing = [[]],
  [''] = [[]],
}

GregorioRef = {}

function GregorioRef.emit_score_glyphs(cs_normal, cs_hollow)
  local common_glyphs = {}
  local normal_variants = {}
  local normal_names = {}
  local hollow_variants = {}
  local hollow_names = {}

  local function index_font(csname, variants, names, common)
    local glyphs = font.fonts[font.id(csname)].resources.unicodes
    -- force-load the code points of the font --
    local ignored = glyphs['___magic___']
    local glyph, cp
    for glyph, cp in pairs(glyphs) do
      names[glyph] = true
      if cp >= 0xe000 and not EXCLUDE[glyph] and not glyph:match('^HEpisema') then
        local name, variant = glyph:match('^([^.]*)(%.%a*)$')
        if name then
          local glyph_variants = variants[name]
          if glyph_variants == nil then
            glyph_variants = {}
            variants[name] = glyph_variants
          end
          glyph_variants[variant] = cp
        elseif common then
          common[glyph] = cp
        end
      end
    end
  end

  index_font(cs_normal, normal_variants, normal_names, common_glyphs)
  index_font(cs_hollow, hollow_variants, hollow_names, common_glyphs)

  local function maybe_emit_glyph(csname, variants, name, variant)
    local cp = variants[name]
    if cp then
      cp = cp[variant]
      if cp then
        tex.sprint(string.format([[&{\%s\char%d}]], csname, cp))
      end
    end
    if not cp then
      tex.sprint(string.format([[&{\tiny\itshape N/A}]], csname, cp))
    end
  end

  local function emit_score_glyph(fusion, shape, ambitus, debilis, liquescence)
    local name = fusion..shape..ambitus..debilis..liquescence
    local char = common_glyphs[name]
    local gabc = GABC[shape] or GABC_AMBITUS_ONE[shape]
    if gabc then
      local fuse_head = ''
      local fuse_tail = ''
      if fusion ~= '' then
        fuse_head = GABC_FUSE[fusion][shape]
        if fuse_head == nil then
          tex.error('No head fusion for '..name)
        end
        if type(fuse_head) == 'table' then
          fuse_head, gabc = fuse_head[1], fuse_head[2]
        end
      end
      local liq = liquescence
      if liq == 'Up' or liq == 'Down' then
        fuse_tail = GABC_FUSE[liq][shape]
        if fuse_tail == nil then
          tex.error('No tail fusion for '..name)
        end
        liq = ''
      end
      gabc = '('..fuse_head..DEBILIS[debilis]..gabc..LIQUESCENCE[liq]..fuse_tail..')'
    else
      texio.write_nl('GregorioRef Warning: missing GABC for '..name)
    end
    local sorted_normal = sort_unique_keys{normal_variants[name]}
    local sorted_hollow = sort_unique_keys{hollow_variants[name]}
    local n = math.max(1, #sorted_normal, #sorted_hollow)
    local emitted = false, i, variant
    for i = 1,n do
      if emitted then
        tex.sprint([[\nopagebreak&&&]])
      else
        tex.sprint(string.format(
            [[{%s\scriptsize %s{\bfseries %s}{\itshape %s}%s%s}&{\ttfamily\small %s}&{\%s\char%d}&]],
            gabc and [[]] or [[\color{red}]],
            fusion, shape, ambitus, debilis, liquescence,
            gabc and gabc:gsub('&&&', '') or '', cs_normal, char
        ))
      end
      variant = sorted_normal[i]
      if variant then
        tex.sprint(string.format([[{\scriptsize %s}]], variant))
        maybe_emit_glyph(cs_normal, normal_variants, name, variant)
      else
        tex.print([[&]])
      end
      if emitted or not hollow_names[name] then
        tex.sprint([[&&&]])
      else
        tex.sprint(string.format(
            [[&{\ttfamily\small %s}&{\%s\char%d}&]],
            gabc and gabc:gsub('&&&', 'r') or '', cs_hollow, char
        ))
      end
      variant = sorted_hollow[i]
      if variant then
        tex.sprint(string.format([[{\scriptsize %s}]], variant))
        maybe_emit_glyph(cs_hollow, hollow_variants, name, variant)
      else
        tex.print([[&]])
      end
      tex.print([[\\]])
      emitted = true
    end
  end

  local glyph_names = {}
  local ambitus = P'One' + P'Two' + P'Three' + P'Four' + P'Five'
  local majuscule = R'AZ'
  local minuscule = R'az'
  local fusion = P'Upper' + P'Lower'
  local debilis = P'InitioDebilis'
  local post_word_liquescentia = P'Nothing' + P'Deminutus' + P'Ascendens' +
      P'Descendens'
  local liquescentia = post_word_liquescentia + P'Up' + P'Down'
  local word = ((majuscule * minuscule^0) - fusion - ambitus - debilis -
      post_word_liquescentia) + ((P'Ascendens' + P'Descendens') * P'Oriscus')
  local liquescence = debilis^-1 * liquescentia^-1
  local pattern = C(fusion^-1) * C(word^1) * C(ambitus^0) * C(debilis^-1) *
      C(liquescentia^-1) * -1
  local only_twos = P'Two'^1 * -1
  local ambitus_one = P'One' * P'Two'^0 * -1
  for name in pairs(common_glyphs) do
    local a, b, c, d, e = pattern:match(name)
    if b then
      table.insert(glyph_names, { a, b, c, d, e })
    else
      -- if parse fails, just use the name
      table.insert(glyph_names, { '', name, '', '', '' })
    end
  end
  local function compare(x, y)
    local nx = x[1]..x[2]
    local ny = y[1]..y[2]
    if nx < ny then
      return true
    elseif nx == ny then
      if x[4] < y[4] then
        return true
      elseif x[4] == y[4] then
        if x[5] < y[5] then
          return true
        elseif x[5] == y[5] and x[3] < y[3] then
          return true
        end
      end
    end
    return false
  end
  table.sort(glyph_names, compare)
  local first = true
  local i, name
  for i, name in ipairs(glyph_names) do
    local shape = name[2]
    local ambitus = name[3]
    if shape:match('^Virgula') or shape:match('^Divisio') then
      shape = shape..ambitus
      ambitus = ''
    end
    if not EXCLUDE[shape] then
      if (ambitus == '' and name[5] == '') or ambitus == '' or only_twos:match(ambitus)
          or (GABC_AMBITUS_ONE[shape] and ambitus_one:match(ambitus)) then
        if first then
          first = false
        else
          tex.print([[\hline]])
        end
        emit_score_glyph(name[1], shape, ambitus, name[4], name[5])
      end
    end
  end
end

function GregorioRef.emit_extra_glyphs(csname)
  local glyphs = font.fonts[font.id(csname)].resources.unicodes
  local first = true
  local odd = true
  for i, name in ipairs(sort_keys(glyphs)) do
    local cp = glyphs[name]
    if cp >= 0xe000 and not EXCLUDE[name] then
      if first then
        first = false
      elseif odd then
        tex.print([[\hline]])
      end
      tex.sprint(string.format([[{\scriptsize %s}&{\%s\char%d}]], name, csname, cp))
      if odd then
        tex.sprint([[&]])
      else
        tex.print([[\\]])
      end
      odd = not odd
    end
  end
  if not odd then
    tex.print([[&\\]])
  end
end

function GregorioRef.emit_dimension(value)
  value = string.gsub(value, '(-?%d+%.%d+)%s*(%a+)', [[\unit[%1]{%2}]])
  value = string.gsub(value, '(-?%d+%.)%s*(%a+)', [[\unit[%1]{%2}]])
  value = string.gsub(value, '(-?%.?%d+)%s*(%a+)', [[\unit[%1]{%2}]])
  tex.sprint(value)
end
