Index: cortav.ct ================================================================== --- cortav.ct +++ cortav.ct @@ -120,11 +120,13 @@ * [*horizontal rule] ([`\---]): inserts a horizontal rule or other context break; does not end the section. must be followed by newline. underlines can also be used in place of dashes ([`___], [`-_-], [`__-__-__] etc), as can horizontal unicode box drawing characters ([`─ ━ ┈] etc). * [*page break] ([`\^^]): for formats that support pagination, like EPUB or HTML (when printed), indicates that the rest of the current page should be blank. for formats that do not, extra margins will be inserted. does not create a new section * [*page rule] ([`\^-^]): inserts a page break for formats that support them, and a horizontal rule for formats that do not. does not create a new section. comprised of any number of horizontal rule characters surrounded by a pair of carets (e.g. [`^-^] [`^_^] [`^----^] [`^__--^] [`^┈┈┈┈┈^]) * [*table cells] ([`+ |]): see [>ex.tab table examples]. * [*equations] ([`=]): block-level equations can be inserted with the [`=] sequence -* [*cross-references] ([`=>] [`⇒]): inserts a block-level link. uses the same syntax as span links ([`⇒[$ident] [$styled-text]]). can be followed by a caption to add a longer descriptive text. especially useful for gemtext output. ident can be omitted to cross-reference, for example, a physical book. +* [*cross-references] ([`=>] [`⇒]): inserts a block-level link. has two forms for the sake of gemtext compatibility. [$styled-text] is a descriptive text of the destination. especially useful for menus and gemtext output. +** the cortav syntax is [`=>[$ident] [$styled-text]], where [$ident] is an identifier; links to the same destination as [` \[>[$ident] [$styled-text]\]] would +** the compatibility syntax is [`=> [$uri] [$styled-text]] (note the space before [$uri]!). instead of taking an identifier for an object in the document, it directly accepts a URI. note that this is not formally equivalent to gemtext's link syntax, which also allows paths in place of URIs; [`cortav] does not. the gemtext line ["=> /somewhere] would need to be expressed as ["=> file:/somewhere], and ["=> /somewhere?key=val] as ["http:/somewhere?key=val] (or ["gemini:/somewhere?key=val], if the result is to be served over a gemini server). * [*empty lines] (that is, lines consisting of nothing but whitespace) constitute a [!break], which terminates multiline objects that do not have a dedicated termination sequence, for example lists and asides. ##onspans styled text most blocks contain a sequence of spans. these spans are produced by interpreting a stream of [*styled-text] following the control sequence. styled-text is a sequence of codepoints potentially interspersed with escapes. an escape is formed by an open square bracket [`\[] followed by a [*span control sequence], and arguments for that sequence like more styled-text. escapes can be nested. Index: cortav.lua ================================================================== --- cortav.lua +++ cortav.lua @@ -1030,11 +1030,11 @@ elseif c == ':' then local lst = l:sub(p.byte-#c,p.byte-#c) local nxt = l:sub(p.next.byte,p.next.byte) if lst == '|' or lst == '+' and l:sub(p.byte-2,p.byte-2) ~= '\\' then buf.align = 'left' - elseif nxt == '|' or nxt == '|' then + elseif nxt == '|' or nxt == '+' then if buf.align == 'left' then buf.align = 'center' else buf.align = 'right' end @@ -1078,15 +1078,17 @@ end end local function insert_link_block(seq) return blockwrap(function(s,c) - local r = s:sub(#seq):gsub('^%s+','') -- chomp - local uri, txt = r:match('^([^%s]*)%s*(.*)$') + local r = s:sub(#seq+1) + local k, uri, txt = r:match('^(%s*)([^%s]*)%s*(.*)$') return { - uri = ss.uri(uri); - label = ct.parse_span(txt, c); + kind = 'link'; + uri = (k~='') and ss.uri(uri) or nil; + ref = (k=='') and uri or nil; + spans = ct.parse_span(txt, c); } end) end ct.ctlseqs = { Index: render/html.lua ================================================================== --- render/html.lua +++ render/html.lua @@ -403,10 +403,50 @@ } h1,h2,h3,h4,h5,h6 { page-break-after: avoid; } ]]; + linkBlock = [[ + a[href].link { + position: relative; + display: block; + padding: .5em; + padding-right: 1.5em; + border: 1px solid @tone(0.2 30); + background: @tone(0.05 30); + font-size: 1.1em; + margin: 0 1em; + text-decoration: none; + color: @tone(0.8 30); + } + a[href].link + a[href].link { + margin-top: -1px; + } + a[href].link:hover { + border-color: @tone(0.3 30); + background: @tone(0.2 30); + color: @tone(0.95 30); + } + a[href].link:hover + a[href].link { + margin-top: 0; + border-top: none; + } + a[href].link::after { + display: block; + position: absolute; + right: .5em; + content: "→"; + top: 50%; + margin-left: 1em; + font-size: 1.8em; + transform: translateY(-50%); + color: @tone(0.3 30); + } + a[href].link:hover::after { + color: @tone(0.7 30); + } + ]]; } local stylesNeeded = { flags = {}; order = {}; @@ -449,10 +489,60 @@ doc.stage.job = renderJob; local runhook = function(h, ...) return renderJob:hook(h, render_state_handle, ...) end + + local function htmlURI(uri) + local family = uri:canfetch() + if family == 'file' then + if uri.namespace == 'localhost' then + -- emit an actual file url + return 'file://' .. uri:construct('path','frag') + elseif uri.namespace == nil then + -- this is gonna be tricky. first we establish the location + -- of the CWD/asset base relative to the output file (if any; + -- assume equivalent otherwise) then express the difference + -- as a directory prefix. + -- jk tho for now we just emit the path+frag sadlol TODO + if uri.path == nil and uri.frag then + -- file:#sec links to #sec within the current document + return uri:part 'frag' + else + return uri:construct('path','frag') + end + else + b.origin:fail('file: URI namespace must be empty or “localhost” for HTML links; others are not meaningful (offending URI: “%s”)', uri.raw) + end + elseif family == 'http' then + local sc = 'http' + if uri.class[1] == 'https' or uri.class[2] == 'tls' then + sc = 'https' + end + if uri.namespace == nil and uri.auth == nil and uri.svc == nil then + -- omit the scheme so we can use a relative path + return uri:construct('path','query','frag') + else + uri.class = {sc} + return tostring(uri) + end + else return tostring(uri) end + end + + local function idLink(id,b) + local dest_o, _, dest_s = b.origin:ref(id) + if dest_o == nil then + -- link is to the section itself + return '#' .. getSafeID(dest_s) + else + if type(dest_o) == 'table' then + return '#' .. getSafeID(dest_o) + else -- URI in reference + return htmlURI(ss.uri(dest_o)) + end + end + end local tagproc do local html_open = function(t,attrs) if attrs then return t .. ss.reduce(function(a,b) return a..b end, '', @@ -613,52 +703,12 @@ function span_renderers.raw(v,b,s) return htmlSpan(v.spans, b, s) end function span_renderers.link(sp,b,s) - local dest_o, _, dest_s = b.origin:ref(sp.ref) - local href - if dest_o == nil then - -- link is to the section itself - href = '#' .. getSafeID(dest_s) - else --- if sp.addr then href = sp.addr else - if type(dest_o) == 'table' then - href = '#' .. getSafeID(dest_o) - else -- URI in reference - local uri = ss.uri(dest_o) - if uri.class[1] == 'file' - or uri.class[1] == 'asset' then - if uri.namespace == 'localhost' then - -- emit an actual file url - href = 'file://' .. uri:construct('path','frag') - elseif uri.namespace == nil then - -- this is gonna be tricky. first we establish the location - -- of the CWD/asset base relative to the output file (if any; - -- assume equivalent otherwise) then express the difference - -- as a directory prefix. - -- jk tho for now we just emit the path+frag sadlol TODO - href = uri:construct('path','frag') - else - b.origin:fail('file: URI namespace must be empty or “localhost” for HTML links; others are not meaningful (offending URI: “%s”)', dest_o) - end - elseif uri:canfetch() == 'http' then - local sc = 'http' - if uri.class[1] == 'https' or uri.class[2] == 'tls' then - sc = 'https' - end - if uri.namespace == nil and uri.auth == nil and uri.svc == nil then - -- omit the scheme so we can use a relative path - href = uri:construct('path','query','frag') - else - uri.class = {sc} - href = tostring(uri) - end - else href = tostring(uri) end - end - end - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href) + local href = idLink(sp.ref,b) + return tag('a',{href=href}, next(sp.spans) and htmlSpan(sp.spans,b,s) or href) end span_renderers['line-break'] = function(sp,b,s) return elt('br') end @@ -775,10 +825,22 @@ end end; ['list-item'] = function(b,s) return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) end; + link = function(b,s) + addStyle 'linkBlock' + local href + if b.uri then + href = htmlURI(b.uri) + elseif b.ref then + href = idLink(b.ref, b) + end + local sp = sr.htmlSpan(b.spans, b, s) + return tag('div', {}, + catenate{tag('a',{class='link', href=href},sp)}) + end; table = function(b,s) local tb = {} for i, r in ipairs(b.rows) do local row = {} for i, c in ipairs(r) do @@ -865,20 +927,10 @@ local obj if b.rsrc then obj = b.rsrc else obj = b.origin:ref(b.ref) end - local function htmlURI(u) - local family = u:canfetch() - if family == 'file' or - (family == 'http' and u.namespace == nil) then - -- TODO asset: - return u.path - else - return tostring(u) - end - end local function uriForSource(s) if s.mode == 'link' or s.mode == 'auto' then return htmlURI(s.uri) elseif s.mode == 'embed' then local mime = s.mime:clone()