michal-h21 / make4ht

Build system for tex4ht
132 stars 15 forks source link

Generating HTML output, but leaving the styling to the user (i.e. don't make CSS?) #83

Closed bzm3r closed 9 months ago

bzm3r commented 1 year ago

A nice feature of make4ht is that it converts the latex document to HTML, but the awesome feature of make4ht is that it generates all the right images and inserts them in the right places for math. The pic-m option in particular is great, and I use all the pic-* options.

What I am finding less nice is the CSS generated. Is there a way for the program to generate usefully class names, but then leave all the styling up to the user? I can look at each class, and then write my CSS to handle styling for each class, rather than have an automatic CSS generated?

michal-h21 commented 1 year ago

You can disable CSS generation using the -css option. But I don't recommend using it, as TeX4ht can use rules for particular elements, like table rules, colored text, or vertical alignment of pictures from the pic-m option. So you want to use at least these. See also this answer for more details.

I also investigated one more option - use make4ht post-processing filters to insert these specific rules as a style attribute to elements where they apply. The following build file does that, and also removes the link to the CSS file, so you need to add a link to your own CSS.

local domfilter = require "make4ht-domfilter"
local cssquery  = require "luaxml-cssquery"

local logging = require "make4ht-logging"

local log = logging.new("build file")

local cssrules = {}
local cssobj   = cssquery()

local function parse_rule(line)
  local selector, values = line:match("%s*(.-)%s*(%b{})")
  if values then
    values = values:sub(2,-2)
  end
  return selector, values
end

local function join_values(old, new)
  -- correctly joins two attribute lists, depending on the ending
  local separator = ";"
  if not old then return new end
  -- if old already ends with ;, then don't use semicolon as a separator
  if old:match(";%s*$") then separator = "" end
  return old .. separator .. new
end

local function parse_css(filename)
  local css_file = io.open(filename, "r")
  if not css_file then return nil, "cannot load css file: " .. (filename or "") end
  local newlines = {}
  for line in css_file:lines() do
    -- match lines that contain # or =, as these can be id or attribute selectors
    if line:match("[%#%=].-{") then
      -- update attributes for the current selector
      local selector, value = parse_rule(line)
      local oldvalue = cssrules[selector] 
      cssrules[selector] = join_values(oldvalue, value)
    else
      newlines[#newlines+1] = line
    end
  end
  -- we need to add css rules
  for selector, value in pairs(cssrules) do
    cssobj:add_selector(selector, function(dom) end, {value=value})
  end
  css_file:close()
  -- write new version of the CSS file, without rules for ids and attributes
  local css_file = io.open(filename, "w")
  css_file:write(table.concat(newlines, "\n"))
  css_file:close()
  return true
end

-- process the CSS file before everything else
local processed = false
Make:match(".*", function(filename, par)
  if processed then return true end
  processed = true
  local css_file = par.input .. ".css"
  local status, msg = parse_css(css_file)
  if not status then  log:warning(msg) end
end)

-- process the HTML file and insert inline CSS for id and attribute selectors
local process = domfilter {
  function(dom, par)
    -- loop over all elements in the current page
    dom:traverse_elements(function(curr)
      -- remove links to the CSS file generated by TeX4ht
      if curr:get_element_name() == "link" 
        and curr:get_attribute("href"):match("^" .. par.input .. "%.css")
      then
         curr:remove_node()
      end
      -- use CSS object to match if the current element
      -- is matched by id attribute selector
      local matched = cssobj:match_querylist(curr)
      if #matched > 0 then
        -- join possible already existing style attribute with values from the CSS file
        local values = curr:get_attribute("style")
        -- join values of all matched rules
        for _,rule in ipairs(matched) do
          values = join_values(values, rule.params.value)
        end
        curr:set_attribute("style", values)
      end

    end)
    return dom
  end
}

Make:match("html$", process)

Compile your file using:

 $ make4ht -e build.lua filename.tex
yalguzaq commented 1 year ago

This would be a nice Q&A for tex.stackexchange.com

michal-h21 commented 1 year ago

I've also added an updated version of this code to make4ht sources as an extension.so it will be possible to use it as:

$ make4ht -f html5+inlinecss foo.tex
bzm3r commented 1 year ago

@michal-h21 You should post your answer here: https://tex.stackexchange.com/questions/663922/make4ht-generating-html-output-but-leaving-the-styling-to-the-user-i-e-don

(and thank you for all your work :)