f164d5cd5b9cfecc0c35f7e08350a2a768e46457 mspeir Wed Sep 16 11:40:45 2026 -0700 Sort attributes in staticPage.lua so the docs build is reproducible, no Redmine attributes() walked the attribute table with pairs(), which has no defined order, so any element with two or more attributes came out in a different order from one build to the next. Eight identical builds of gb101.md produced two distinct outputs. It renders the same either way, but it makes the install rsync re-push files nobody edited and makes a byte comparison of any docs change meaningless. Sorting the keys fixes it; no page changes semantically. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> diff --git docs/staticPage.lua docs/staticPage.lua index d6ff5208871..d07680a155d 100644 --- docs/staticPage.lua +++ docs/staticPage.lua @@ -1,484 +1,494 @@ -- UCSC: Slightly modified to produce html that follows the genome browser -- static pages conventions -- -- -- This is a sample custom writer for pandoc. It produces output -- that is very similar to that of pandoc's HTML writer. -- There is one new feature: code blocks marked with class 'dot' -- are piped through graphviz and images are included in the HTML -- output using 'data:' URLs. -- -- Invoke with: pandoc -t staticPage.lua -- -- Character escaping local function escape(s, in_attribute) return s:gsub("[<>&\"']", function(x) if x == '<' then return '<' elseif x == '>' then return '>' elseif x == '&' then return '&' elseif x == '"' then return '"' elseif x == "'" then return ''' else return x end end) end -- Helper function to convert an attributes table into -- a string that can be put into HTML tags. +-- The keys are sorted first because pairs() has no defined order, so an element +-- with more than one attribute would otherwise come out in a different order +-- from one build to the next. That renders the same, but it makes the output +-- unreproducible and leaves the install rsync re-pushing files nobody edited. local function attributes(attr) + local keys = {} + for x in pairs(attr) do + table.insert(keys, x) + end + table.sort(keys) local attr_table = {} - for x,y in pairs(attr) do + for _,x in ipairs(keys) do + local y = attr[x] if y and y ~= "" then table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"') end end return table.concat(attr_table) end -- Run cmd on a temporary file containing inp and return result. local function pipe(cmd, inp) local tmp = os.tmpname() local tmph = io.open(tmp, "w") tmph:write(inp) tmph:close() local outh = io.popen(cmd .. " " .. tmp,"r") local result = outh:read("*all") outh:close() os.remove(tmp) return result end -- Table to store footnotes, so they can be included at the end. local notes = {} -- Table to store headers, so they can be inserted at the end local headers = {} -- flag to see if we have to close a previous div local headerOpen = false -- Blocksep is used to separate block elements. function Blocksep() return "\n\n" end -- Print contents of `tbl`, with indentation. -- `indent` sets the initial level of indentation. function tprint (tbl, indent) if not indent then indent = 0 end for k, v in pairs(tbl) do formatting = string.rep(" ", indent) .. k .. ": " if type(v) == "table" then print(formatting) tprint(v, indent+1) elseif type(v) == 'boolean' then print(formatting .. tostring(v)) else print(formatting .. v) end end end -- remove all special chars from the string and remove all html tags function simplifyId(idStr) idStr=idStr:gsub("%b<>", "") idStr = string.lower(string.gsub(idStr, "%s+", "-")) idStr = string.lower(string.gsub(idStr, "[^a-zA-z-]", "")) return idStr end -- This function is called once for the whole document. Parameters: -- body is a string, metadata is a table, variables is a table. -- This gives you a fragment. You could use the metadata table to -- fill variables in a custom lua template. Or, pass `--template=...` -- to pandoc, and pandoc will add do the template processing as -- usual. function Doc(body, metadata, variables) local buffer = {} local function add(s) table.insert(buffer, s) end -- ucsc changes start -- debugging of meta info: -- print("vars") -- tprint(variables) -- print("meta") -- tprint(metadata) if metadata["title"] then add("<!DOCTYPE HTML>") add("<!-- DO NOT EDIT THE HTDOCS VERSION OF THIS FILE. THIS FILE IS AUTOMATICALLY") add(" GENERATED FROM A MARKDOWN FILE IN kent/src/product. MAKE ANY EDITS TO") add(" THIS PAGE THERE, THEN RUN MAKE in kent/src/product/mirrorDocs, AND FOLLOW THE") add(" INSTRUCTIONS TO COMMIT THIS PAGE INTO git.") add(" Please read the file kent/src/product/Note-To-QA.txt for details.") add(" -->") add("<!--#set var=\"TITLE\" value=\"" .. metadata["title"] .. "\" -->") add("<!--#set var=\"ROOT\" value=\"" .. (variables["ROOT"] or metadata["ROOT"] or "..") .. "\" -->") add("") add("<!-- Relative paths to support mirror sites with non-standard GB docs install -->") add("<!--#include virtual=\"$ROOT/inc/gbPageStart.html\" -->") add("<link rel=\"stylesheet\" href=\"<!--#echo var=\"ROOT\" -->/style/bootstrap-3-3-7.min.css\">") add("") add("<h1>" .. metadata["title"] .. "</h1>") else add("<h1>No title defined in document, first line must be % mytitle </h1>") end -- Sidebar TOC + body in Bootstrap grid. The docs-page class scopes -- /docs/-only typography and table styling in gbStatic.css. add('<div class="row docs-toc-row docs-page">') add('<div class="col-md-2 hidden-sm hidden-xs">') add('<nav class="docs-toc" id="docs-toc">') add('<ul>') for i, h in ipairs(headers) do local cls = h.sub and " class='docs-toc-sub'" or "" add("<li" .. cls .. "><a href='#" .. h.id .. "'>" .. h.text .. "</a></li>") end add('</ul>') add('</nav>') add('</div>') add('<div class="col-md-10">') -- ucsc change end add(body) add('</div>') -- close col-md-10 add('</div>') -- close row if #notes > 0 then add('<ol class="footnotes">') for _,note in pairs(notes) do add(note) end add('</ol>') end return table.concat(buffer,'\n') .. '\n' end -- The functions that follow render corresponding pandoc elements. -- s is always a string, attr is always a table of attributes, and -- items is always an array of strings (the items in a list). -- Comments indicate the types of other variables. function Str(s) return escape(s) end function Space() return " " end function SoftBreak() return "\n" end function LineBreak() return "<br/>" end function Emph(s) return "<em>" .. s .. "</em>" end function Strong(s) return "<strong>" .. s .. "</strong>" end function Subscript(s) return "<sub>" .. s .. "</sub>" end function Superscript(s) return "<sup>" .. s .. "</sup>" end function SmallCaps(s) return '<span style="font-variant: small-caps;">' .. s .. '</span>' end function Strikeout(s) return '<del>' .. s .. '</del>' end function obfuscate(s) -- obfuscate the email address a la Hiram's encodeEmail.pl local refs = {} for i = 1, string.len(s) do local ascCode = string.byte(s, i) local ref = "&#" .. tostring(ascCode) table.insert(refs, ref) end return table.concat(refs, "") end function Link(s, src, tit, attr) local extra = "" if string.sub(src, 1, 7)=="mailto:" then src = "mailto:" .. obfuscate(string.sub(src, 7), 7) -- assume that the link label is an email address if it contains an @ character if string.match(s, "%@")~=nil then s = obfuscate(s) end else -- absolute URLs open in a new tab; rel="noopener" prevents tabnabbing if string.match(src, "^https?://") then extra = " target='_blank' rel='noopener'" end src = escape(src, true) tit = escape(tit, true) end if tit and tit ~= "" then extra = extra .. " title='" .. tit .. "'" end return "<a href='" .. src .. "'" .. extra .. ">" .. s .. "</a>" end function Image(s, src, tit, attr) return "<img alt='" .. escape(s,true) .. "' src='" .. escape(src,true) .. "' title='" .. escape(tit,true) .. "'/>" end function Code(s, attr) return "<code" .. attributes(attr) .. ">" .. escape(s) .. "</code>" end function InlineMath(s) return "\\(" .. escape(s) .. "\\)" end function DisplayMath(s) return "\\[" .. escape(s) .. "\\]" end function SingleQuoted(s) return "‘" .. s .. "’" end function DoubleQuoted(s) return "“" .. s .. "”" end function Note(s) local num = #notes + 1 -- insert the back reference right before the final closing tag. s = string.gsub(s, '(.*)</', '%1 <a href="#fnref' .. num .. '">↩</a></') -- add a list item with the note to the note table. table.insert(notes, '<li id="fn' .. num .. '">' .. s .. '</li>') -- return the footnote reference, linked to the note. return '<a id="fnref' .. num .. '" href="#fn' .. num .. '"><sup>' .. num .. '</sup></a>' end function Span(s, attr) return "<span" .. attributes(attr) .. ">" .. s .. "</span>" end function Cite(s, cs) local ids = {} for _,cit in ipairs(cs) do table.insert(ids, cit.citationId) end return "<span class=\"cite\" data-citation-ids=\"" .. table.concat(ids, ",") .. "\">" .. s .. "</span>" end function Plain(s) return s end function Para(s) return "<p>\n" .. s .. "\n</p>" end -- lev is an integer, the header level. function Header(lev, s, attr) local lines = { } if lev == 1 then idStr = simplifyId(s) table.insert(headers, {text = s, id = idStr, sub = false}) table.insert(lines, "<h2 id='" .. idStr .. "'>" .. s .. "</h2>") headerOpen = true elseif lev == 2 then -- Collect lev==2 headers for TOC (markdown ## headings) local idStr = attr.id or simplifyId(s) table.insert(headers, {text = s, id = idStr, sub = false}) table.insert(lines, "<h" .. lev .. " id='" .. idStr .. "'" .. ">" .. s .. "</h" .. lev .. ">") elseif lev == 3 then -- Collect lev==3 headers for TOC as indented sub-items (markdown ### headings) local idStr = attr.id or simplifyId(s) table.insert(headers, {text = s, id = idStr, sub = true}) table.insert(lines, "<h" .. lev .. " id='" .. idStr .. "'" .. ">" .. s .. "</h" .. lev .. ">") else table.insert(lines, "<h" .. lev .. attributes(attr) .. ">" .. s .. "</h" .. lev .. ">") end return table.concat(lines, "\n") end function BlockQuote(s) return "<blockquote>\n" .. s .. "\n</blockquote>" end function HorizontalRule() return "<hr/>" end function CodeBlock(s, attr) -- If code block has class 'dot', pipe the contents through dot -- and base64, and include the base64-encoded png as a data: URL. if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then local png = pipe("base64", pipe("dot -Tpng", s)) return '<img src="data:image/png;base64,' .. png .. '"/>' -- otherwise treat as code (one could pipe through a highlighter) else return "<pre><code" .. attributes(attr) .. ">" .. escape(s) .. "</code></pre>" end end function BulletList(items) local buffer = {} for _, item in pairs(items) do table.insert(buffer, "<li>" .. item .. "</li>") end return "<ul>\n" .. table.concat(buffer, "\n") .. "\n</ul>" end function OrderedList(items) local buffer = {} for _, item in pairs(items) do table.insert(buffer, "<li>" .. item .. "</li>") end return "<ol>\n" .. table.concat(buffer, "\n") .. "\n</ol>" end -- Revisit association list STackValue instance. function DefinitionList(items) local buffer = {} for _,item in pairs(items) do for k, v in pairs(item) do table.insert(buffer,"<dt>" .. k .. "</dt>\n<dd>" .. table.concat(v,"</dd>\n<dd>") .. "</dd>") end end return "<dl>\n" .. table.concat(buffer, "\n") .. "\n</dl>" end -- Convert pandoc alignment into a style attribute fragment for HTML5. -- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault. Left is the -- default cell alignment, so emit nothing in that case. function html_align_style(align) if align == 'AlignRight' then return ' style="text-align: right"' elseif align == 'AlignCenter' then return ' style="text-align: center"' else return '' end end function CaptionedImage(src, tit, caption) return '<div class="figure">\n<img src="' .. escape(src,true) .. '" title="' .. escape(tit,true) .. '"/>\n' .. '<p class="caption">' .. caption .. '</p>\n</div>' end -- Caption is a string, aligns is an array of strings, -- widths is an array of floats, headers is an array of -- strings, rows is an array of arrays of strings. function Table(caption, aligns, widths, headers, rows) local buffer = {} local function add(s) table.insert(buffer, s) end add("<table>") if caption ~= "" then add("<caption>" .. caption .. "</caption>") end if widths and widths[1] ~= 0 then for _, w in pairs(widths) do add('<col style="width: ' .. string.format("%.0f%%", w * 100) .. '">') end end local header_row = {} local empty_header = true for i, h in pairs(headers) do table.insert(header_row,'<th' .. html_align_style(aligns[i]) .. '>' .. h .. '</th>') empty_header = empty_header and h == "" end if empty_header then head = "" else add('<tr class="header">') for _,h in pairs(header_row) do add(h) end add('</tr>') end local class = "even" for _, row in pairs(rows) do class = (class == "even" and "odd") or "even" add('<tr class="' .. class .. '">') for i,c in pairs(row) do add('<td' .. html_align_style(aligns[i]) .. '>' .. c .. '</td>') end add('</tr>') end add('</table>') return table.concat(buffer,'\n') end function RawInline(format, str) if format == "html" then return str else --- not sure what to do here for PDF or ebook output... --- return str end end function RawBlock(format, str) if format == "html" then return str else return '' end end function Div(s, attr) return "<div" .. attributes(attr) .. ">\n" .. s .. "</div>" end -- The following code will produce runtime warnings when you haven't defined -- all of the functions you need for the custom writer, so it's useful -- to include when you're working on a writer. local meta = {} meta.__index = function(_, key) io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key)) return function() return "" end end setmetatable(_G, meta)