Warning: this is an htmlized version!
The original is here, and
the conversion rules are here.
-- elisp.lua: parse and interpret sexp hyperlinks.
-- This file:
--   http://angg.twu.net/blogme4/elisp.lua.html
--   http://angg.twu.net/blogme4/elisp.lua
--                (find-blogme4 "elisp.lua")
-- Author: Eduardo Ochs <[email protected]>
-- Version: 2011aug01
-- License: GPL3
--

-- The docs below are a mess!!!
--
-- Let me start by supposing that you know what a sexp is. Then you
-- know what a "sexp one-liner" is, and I will say that a line "has an
-- elink" if it is made of some "prefix characters" (possibly zero),
-- then a sexp (a list), then optional spaces.
-- The "sexp hyperlinks" used by eev are elinks.
-- See: (find-eevarticlesection "hyperlinks")
--      http://en.wikipedia.org/wiki/S-expression
--
-- One of the hardest parts of htmlzing the material in
-- http://angg.twu.net/ is that many of the files there require
-- htmlizing "sexp hyperlinks", like this one:
--
--      (find-blogme4 "def.lua" "undollar")
--
-- the htmlization makes the "find-blogme4" into a link to a section
-- of the documentation about eev, and makes the two last chars of the
-- sexp, '")', behave somehow like what the sexp would do when run in
-- Emacs...

--   (find-blogme4 "hyperlinks")

-- Here is a rough sketch of what we need to do on each line that may
-- end with a sexp ("rough sketch" means "the details are below,
-- scattered around")... We need to:
--
--   1) detect whether that line ends with a sexp,
--   2) split each line that ends with a sexp into what comes before
--      the sexp (we call that the "pre"), the hyperlink itself (the
--      "sexp") and the optional spaces after the sexp ("the spaces"),
--   3) split the sexp into its "elements",
--   4) check whether the first "element" (the "head") is a symbol,
--   5) check whether the "head" has an entry in the table "ewords",
--   6) if it has, we need to run sexp:sexphtml(), that usually:
--     7) splits the sexp into an "opening parenthesis" (the "o"),
--        the "word" (the "w"), the "rest" ("r") and the "close"
--        (usually the two last chars - '")'),
--     8) determines the "help url" that will be associated to the
--        "word" and the "target url" that will be associated to the
--        "close",
--     9) compose "o", "w", "r", "c" and the help url and the target
--        url to build an htmlization of the sexp,
--
-- I found a nice hackish way to detect if a line "has an elink".
-- The algorithm is non-recursive, does not backtrack, runs very
-- quickly, and can be implemented in Lua using just string.gsub,
-- string.reverse and string.match. And it doesn't need Lpeg!...
--
-- The rough idea is:
--   1) first simplify all literal strings - like "foo bar" - by:
--    1a) replacing all backslash-char pairs by "__"s, and then
--    1b) replacing all chars inside double-quotes by "_"s;
--   2) then, starting from the right, use Lua's "%b" pattern to find
--    matching "()"s.
--
-- Part of the trick is that we use string.reverse judiciously at the
-- right points of the algorithm to perform pattern matches "starting
-- from the right". Also, we produce a "simplified string" and work on
-- it, but we keep the original string (that has the same length as
-- the simplified one), and after doing all the parsing and
-- discovering where the sexp and all its "elements" start and end we
-- go back to the original string.
--
-- Here's an example that illustrates how the algorithm works.
--   line = [[ # (foo "a") (bar "plic: \"ploc\"") ]]
--
--   skel = [[ # (foo "a") (bar "plic: __ploc__") ]]
--   leks = [[ # (foo "_") (bar "______________") ]]:reverse()
--   secaps =                                  [[ ]]:reverse()
--   lekspxes =          [[(bar "______________")]]:reverse()
--   sexpskel =          [[(bar "______________")]]
--   erp  = [[ # (foo "_") ]]:reverse()
--
--   pre  = [[ # (foo "a") ]]
--   sexp =              [[(bar "plic: \"ploc\"")]]
--   spaces =                                  [[ ]]
--      1 =            {0=[[bar]], 2, 5},
--      2 =                {0=[["plic: \"ploc\""]], 6, 22}
--
--      o =              [[(]]
--      w =               [[bar]]
--      r =                  [[ "plic: \"ploc\"]]
--      c =                                  [[")]]


require "eoo"    -- (find-blogme4 "eoo.lua")
require "common" -- (find-blogme4 "common.lua")

Q = Q or id      -- (find-blogme4 "anggdefs.lua" "Q")


-- Some utility functions
notdir = function (str) return str:match "[^/]$" end   -- "" is a directory
addfileext = function (fname, ext)
    if fname and ext and notdir(fname) then return fname..ext end
    return fname
  end
addanchor = function (url, anchor)
    if url and anchor then return url.."#"..anchor end
    return url
  end
addextanchor = function (fname, ext, anchor)
    return addanchor(addfileext(fname, ext), anchor)
  end


href_ = function (url, text)
    if url then return "<a href=\""..url.."\">"..text.."</a>" end
    return text
  end
buildurl_ = function (base, offset, ext, anchor)
    if not offset then return end
    local url = base..offset
    if notdir(url) and ext then url = url..ext end
    if anchor then url = url.."#"..anchor end
    return url
  end


Sexpline = Class {
  type    = "Sexpline",
  __index = {
    -- Two functions to split fields, calculating new fields.
    -- This one splits "line" into "pre", "sexp", and "spaces",
    -- and, as a bonus, it obtain the "elements" of the sexp
    -- (stored in integer-indexed positions).
    presexpspaces_ = function (sexpline)
        local line = sexpline.line
        local skel = line:gsub("\\.", "__")
        local leks = skel:reverse()
        local f = function (s) return '"'..("_"):rep(#s)..'"' end
        local leks = leks:gsub('"([^"]-)"', f)
        local secaps, lekspxes, erp = leks:match("^([ \t]*)(%b)()(.*)")
        if not erp then return end
        local pre      = line:sub(1, #erp)
        local sexpskel = lekspxes:reverse()
        local sexp     = line:sub(1+#pre, #pre+#sexpskel)
        local spaces   = secaps:reverse()
        -- bonus: split the "sexp" into its "elements" and store them
        -- as tables in integer-indexed fields in the sexpline structure.
        local n, pos = 0, 2
        local parseelement = function (pat)
            local s, e = sexpskel:match(pat, pos)
            if s then
              n = n + 1
              sexpline[n] = {s, e, [0]=sexp:sub(s, e-1)}
              pos = e
              return true
            end
          end
        while parseelement "^[ \t]*()[^ \t\"()]+()"   -- symbol or number
           or parseelement "^[ \t]*()\"_*\"()"        -- string
           or parseelement "^[ \t]*()%b()()" do       -- list
        end
        sexpline.pre    = pre
        sexpline.sexp   = sexp
        sexpline.spaces = spaces
        return true
      end,
    -- This one splits the "sexp" field into "o", "w", "r", "c"
    -- (for the standard way of htmlizing sexp hyperlinks).
    owrc_ = function (sexpline)
        if sexpline.sexp then
          local pat = "^(%()([-!$%&*+,/:<=>?@^_0-9A-Za-z]+)(.-)(\"?%))$"
          local o, w, r, c = sexpline.sexp:match(pat)  -- open, word, rest, close
          sexpline.o = o
          sexpline.w = w
          sexpline.r = r
          sexpline.c = c
          return true
        end
      end,
    -- Notice the logical gap here! "sexphtml__" uses the fields
    -- "helpurl" and "targeturl", that are set by "sexpurls_" (below).
    -- About specials (like images): they're not supported yet!
    sexphtml__ = function (sexpline)
        if sexpline.helpurl or sexpline.targeturl then
          sexpline.sexphtml =            sexpline.o   ..
             href_(sexpline.helpurl,   Q(sexpline.w)) ..
                                       Q(sexpline.r)  ..
             href_(sexpline.targeturl,   sexpline.c )
	  return true
        end
      end,
    linehtml__ = function (sexpline, htmlizer)
        htmlizer = htmlizer or Q
        if sexpline.sexphtml then
          sexpline.linehtml = htmlizer(sexpline.pre) ..
                              sexpline.sexphtml ..
                              sexpline.spaces
        else
          sexpline.linehtml = htmlizer(sexpline.line)
        end
        return sexpline
      end,
    --
    sexphtml_ = function (sexpline, htmlizer)
        return sexpline:presexpspaces_()
           and sexpline:eword_()
           and sexpline:sexpurls_()  -- defined below
           and sexpline:owrc_()
           and sexpline:sexphtml__(htmlizer)
      end,
    linehtml_ = function (sexpline, htmlizer)
        sexpline:sexphtml_()
        sexpline:linehtml__()
        return sexpline
      end,
    --
    -- Two functions to extract the "elements" of the sexp, as strings.
    -- Like this, but 1-based and typed: (find-elnode "List Elements" "nth")
    symbol = function (sexpline, n)
        return sexpline[n] and sexpline[n][0]:match"^([^()\"].*)$"
      end,
    string = function (sexpline, n)
        return sexpline[n] and sexpline[n][0]:match"^\"(.*)\"$"
      end,
    --
    eword_ = function (sexpline)
        sexpline.word  = sexpline:symbol(1)
        sexpline.eword = ewords[sexpline.word]
        return sexpline.eword
      end,
    sexpurls_ = function (sexpline)
        local eword = sexpline.eword
        if eword then
          local a, b = sexpline:string(2), sexpline:string(3)
          -- no specials yet
          sexpline.helpurl   = eword:helpurl_()
          sexpline.targeturl = eword:targeturl_(a, b)
          -- return sexpline.helpurl, sexpline.targeturl
          return true
        end
      end,
  },
}

Eword = Class {
  type    = "Eword",
  __index = {
    helpurl_ = function (eword) return eword.help end,
    targeturl_ = function (eword, a, b)
        return eword.base and eword:f(a, b) end,
    f = function (eword, a, b)
        return addextanchor(a and eword.base..a, eword.ext, b) end,
  },
}

ewords = {}

htmlizeline_ = function (line, htmlizer)
    return (Sexpline {line=line}):linehtml_(htmlizer)
  end
htmlizeline = function (line, htmlizer)
    return (Sexpline {line=line}):linehtml_(htmlizer).linehtml
  end
htmlizelines = function (bigstr, htmlizer)
    local f = function (line) return htmlizeline(line, htmlizer) end
    return bigstr:gsub("[^\n]*", f)
  end





--                          
--   __ _ _ __   __ _  __ _ 
--  / _` | '_ \ / _` |/ _` |
-- | (_| | | | | (_| | (_| |
--  \__,_|_| |_|\__, |\__, |
--              |___/ |___/ 

targeturl_base_a = function (eword, a, b)
    return a and eword.base..a   -- use just the a
  end
targeturl_to = function (eword, a, b)
    return a and "#"..a
  end

eevarticle = eevarticle or "http://angg.twu.net/eev-article.html"

--[[
ewords["to"] = Eword {
    help = eevarticle.."#anchors",
    -- base = "",
    -- targeturl = function (eword, sexp)
    --     local anchor = sexp:string(2)
    --     if anchor then return "#"..anchor end
    --   end,
  }
--]]

Ew = function (ew)
    ew.help = ew.help or eevarticle.."#shorter-hyperlinks"
    return Eword(ew)
  end
Ewa = function (ew)
    ew.targeturl = targeturl_base_a
    return Ew(ew)
  end

ewords["to"] = Ew {
    help = eevarticle.."#anchors",
    targeturl_ = targeturl_to,
  }

code_c_d_angg   = function (c, d) code_c_d_remote(c, pathto(d)) end
code_c_d_remote = function (c, d)
    ewords["find-"..c.."file"] = Ewa {base = d}
    ewords["find-"..c]         = Ew  {base = d, ext = ".html"}
    ewords["find-"..c.."w3m"]  = Ewa {base = d}
  end

code_c_d_angg("angg",    "")                  -- (find-angg "blogme4/")
code_c_d_angg("es",      "e/")                -- (find-es "lua5")
code_c_d_angg("dednat4", "dednat4/")          -- (find-dednat4 "")
code_c_d_angg("dn4",     "dednat4/")
code_c_d_angg("dn4ex",   "dednat4/examples/")
code_c_d_angg("dn5",     "dednat5/")
code_c_d_angg("blogme",  "blogme/")
code_c_d_angg("blogme3", "blogme3/")
code_c_d_angg("blogme4", "blogme4/")
code_c_d_angg("eev",     "eev-current/")
code_c_d_angg("flua",    "flua/")
code_c_d_angg("rubyforth", "rubyforth/")
code_c_d_angg("vtutil",  "vtutil/")
code_c_d_angg("vtutil4", "vtutil4/")
code_c_d_angg("RETRO",   "RETRO/")

ewords["find-es"].ext = ".e.html"



-- dump-to: tests
-- (find-blogme4 "angglisp.lua")

--  _____         _      __                  _   _                 
-- |_   _|__  ___| |_   / _|_   _ _ __   ___| |_(_) ___  _ __  ___ 
--   | |/ _ \/ __| __| | |_| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
--   | |  __/\__ \ |_  |  _| |_| | | | | (__| |_| | (_) | | | \__ \
--   |_|\___||___/\__| |_|  \__,_|_| |_|\___|\__|_|\___/|_| |_|___/
--                                                                 
elinksplittest1 = function (line)
    local pre, sexp, spaces, elements = elinksplit_(line)
    if not pre then return end
    local chars = {}
    local absrange = function (s, e, char) for i=s,e-1 do chars[i]=char end end
    local range = function (s, e, char) absrange(#pre+s, #pre+e, char) end
    absrange(1,            1+#pre, "p")
    absrange(1+#pre,       1+#pre+#sexp, "-")
    absrange(1+#pre+#sexp, 1+#pre+#sexp+#spaces, "s")
    for i,elt in ipairs(elements) do range(elt[1], elt[2], i) end
    return table.concat(chars)
  end
elinksplittest = function (bigstr)
    for _,line in ipairs(splitlines(bigstr)) do
      print(" -- [["..line.."]]")
      local ranges = elinksplittest1(line)
      if ranges then print(" --   "..ranges) end
    end
  end

transpose = function (T)
    local TT = {}
    for k,v in pairs(T) do TT[v] = k end
    return TT
  end

sortedpairs = function (T, K)
    local P = {}
    local add = function (k) table.insert(P, {key=k, val=T[k]}) end
    K = (type(K) == "string" and split(K)) or K or {}
    local KT = transpose(K)
    for _,k in ipairs(K) do add(k) end
    for _,pair in ipairs(tos_sorted_pairs(T)) do
      if not KT[pair.key] then add(pair.key) end
    end
    return P
  end
sexppairs = function (sexp)
    return sortedpairs(sexp, [[ line pre sexp spaces word eword o w r c
      helpurl targeturl  sexphtml linehtml ]])
  end
isexppairs_ = function (sexp) return ipairs(sexppairs(sexp)) end
isexppairs  = function (line) return ipairs(sexppairs(htmlizeline_(line))) end
isp = function (line) 
    for _,kv in isexppairs(line) do
      if kv.val then print(kv.key.." = "..tos(kv.val)) end
    end
  end

--  _____         _       
-- |_   _|__  ___| |_ ___ 
--   | |/ _ \/ __| __/ __|
--   | |  __/\__ \ |_\__ \
--   |_|\___||___/\__|___/
--                        

--[==[
* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
eevarticle = "eev-article.html"
dofile "elisp.lua"
li = [[ foo (to "plic") ]]
PP(htmlizeline_(li))

se = Sexpline {line = li}
PP(se:presexpspaces_()); PP(se)
PP(se:eword_()        ); PP(se)
PP(se:sexpurls_()     ); PP(se)
PP(se:owrc_()         ); PP(se)
PP(se:sexphtml_(htmlizer)); PP(se)


isp [[ foo (to "plic") ]]

for _,kv in isexppairs(" foo (bar plic) ") do
  PP(kv)
end
for _,kv in isexppairs([[ foo (to "bar") ]]) do
  if kv.val then print(kv.key.." = "..tos(kv.val)) end
end





PP(elinksplit " foo (bar plic) ")  --> {
--     line  =" foo (bar plic) ",
--     pre   =" foo "           ,
--     sexp  =     "(bar plic)" ,
--     spaces=               " ",
--             1={0="bar",        2, 5},
--                 2={0="plic",   6, 10},
--   }


elinksplittest [[
  For lines with elinks, like the one below,
  # (foo "a") (bar "plic: \"ploc\"")
  the test function shows a "range dump".
]]
 --> [[  For lines with elinks, like the one below,]]
 --  [[  # (foo "a") (bar "plic: \"ploc\"")]]
 --    pppppppppppppp-111-2222222222222222-
 --  [[  the test function shows a "range dump".]]




* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
require "elisp"
for li in io.lines("build.lua") do
  local sexp = elinksplit(li)
  local symbol = sexp:symbol(1)
  if symbol then
    local a, b = sexp:string(2), sexp:string(3)
    print(sexp.sexp)
    PP(a, b)
  end
end
-- (find-fline "build.lua")

-- for _,li in ipairs(splitlines(readfile "dednat5/README")) do tt(li) end
-- map(tt, splitlines(readfile "dednat5/README"))
-- tt [[ # (find-fline "foo") (find-fline "bar") ]]


* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
dofile "elisp.lua"
el = elinksplit [[# (find-angg "blogme3/elisp.lua")]]
PP(el)
PP(el:sexpsplit())
el = elinksplit [[# (find-angg "blogme3/")]]
print(el:sexphtml())
el = elinksplit [[# (find-angg "blogme3/elisp.lua")]]
print(el:sexphtml())
el = elinksplit [[# (find-angg "blogme3/elisp.lua" "foo")]]
print(el:sexphtml())
el = elinksplit [[# (find-image "foo.jpg")]]
print(el:sexphtml())



* (eepitch-lua51)
* (eepitch-kill)
* (eepitch-lua51)
require "elisp"
s = Sexpline { line = [[ # (to "targ") ]] }
= s:get_linehtml()
PP(s)

mykeys = split [[ line pre sexp spaces word eword o w r c
  helpurl targeturl
  sexphtml linehtml ]]
mykeyst = transpose(mykeys)
for _,k in ipairs(mykeys) do
  print(" "..k.."="..tos(s[k])..",")
end
for _,p in ipairs(tos_sorted_pairs(s)) do
  if not mykeyst[p.key] then
    print(" "..p.key.."="..tos(p.val)..",")
  end
end

PP(s.eword)
PP(s.eword:get_targeturl("targ", "b"))

 line=" # (to \"targ\") ",
 pre =" # ",
 sexp=   "(to \"targ\")",
 spaces=              " ",
 1=    {0="to", 1=2, 2=4},
 2=       {0="\"targ\"", 1=5, 2=11},
 word=    "to",
 eword={"help"="http://angg.twu.net/eev-article.html#anchors", "targeturl"=<function: 0x8f45f78>},
 helpurl="http://angg.twu.net/eev-article.html#anchors",
 o="(",
 w="to",
 r=" \"targ",
 c="\")",
 sexphtml="(<a href=\"http://angg.twu.net/eev-article.html#anchors\">to</a> \"targ\")",
 linehtml=" # (<a href=\"http://angg.twu.net/eev-article.html#anchors\">to</a> \"targ\") ",




--]==]







-- Local Variables:
-- coding:             raw-text-unix
-- ee-anchor-format:   "«%s»"
-- End: