06b9de0132c8653afc3c2d91cbc386c2f3967e72
lrnassar
  Tue Apr 28 12:05:20 2026 -0700
QA cleanup of /docs/ landing page and staticPage.lua writer. refs #36894

docs/index.md: fix /docs/contacts.html -> /contacts.html (was 404), correct
"Hubs Basics" -> "Hub Basics", drop stray "?" in fragment-only URLs
(/cgi-bin/hgHubConnect?#hubDeveloper etc.), convert section headings to
sentence case per GB convention, rewrite tutorial-table descriptions in a
consistent imperative voice, and replace the fragmented "Checked using our
hub development tools" bullet with "Validate your hub with our hub
development tools".

docs/staticPage.lua: extend Link() to add target="_blank" rel="noopener"
on http(s):// absolute URLs, and stop emitting empty title='' on every
link. Modernize Table() output to drop deprecated HTML4 attributes:
<col width="X%"> -> <col style="width: X%">, and align="left|right|center"
on <th>/<td> replaced with style="text-align: ..." (omitted for the default
left alignment). Renames html_align() -> html_align_style() to reflect the
new return value.

diff --git docs/staticPage.lua docs/staticPage.lua
index 3fc86255de3..615beda2a3d 100644
--- docs/staticPage.lua
+++ docs/staticPage.lua
@@ -208,43 +208,51 @@
   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 .. "' title='" .. tit .. "'>" .. s .. "</a>"
+  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) .. "\\)"
@@ -358,89 +366,87 @@
   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 to something HTML can use.
--- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault.
-function html_align(align)
-  if align == 'AlignLeft' then
-    return 'left'
-  elseif align == 'AlignRight' then
-    return 'right'
+-- 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 'center'
+    return ' style="text-align: center"'
   else
-    return 'left'
+    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 width="' .. string.format("%.0f%%", w * 100) .. '" />')
+      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
-    local align = html_align(aligns[i])
-    table.insert(header_row,'<th align="' .. align .. '">' .. h .. '</th>')
+    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 align="' .. html_align(aligns[i]) .. '">' .. c .. '</td>')
+      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