Overview
Comment: | get math parser working |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
d1b7d2fd5f86585c3ffac19191559cb7 |
User & Date: | lexi on 2021-12-26 17:49:50 |
Other Links: | manifest | tags |
Context
2021-12-26
| ||
20:01 | add C wrapper to generate true standalone binary embedding cortav bytecode check-in: a4a0570841 user: lexi tags: trunk | |
17:49 | get math parser working check-in: d1b7d2fd5f user: lexi tags: trunk | |
04:08 | all kindsa shit check-in: 52b9bce7dd user: lexi tags: trunk | |
Changes
Modified cortav.lua from [028f351fed] to [5feb0b86b1].
394 394 table.insert(ret, (hookfn(me:delegate(ext),...))) 395 395 end 396 396 return ret 397 397 end; 398 398 }; 399 399 } 400 400 401 --- renderer engines 402 -function ct.render.html(doc, opts) 403 - local doctitle = opts['title'] 404 - local f = string.format 401 +-- common renderer utility functions 402 +ct.tool = {} 403 + 404 +function ct.tool.mathfmt(ctx, eqn) 405 + local buf = '' 406 + local m = ss.enum {'num','var','op'} 407 + local lsc = 0 408 + local spans = {} 409 + 410 + local flush = function() 411 + local o 412 + if buf ~= '' then 413 + if lsc == 0 then 414 + o = buf 415 + elseif lsc == m.num then 416 + o = { 417 + kind = 'format'; 418 + style = 'literal'; 419 + spans = {buf}; 420 + } 421 + elseif lsc == m.var then 422 + o = { 423 + kind = 'format'; 424 + style = 'variable'; 425 + spans = {buf}; 426 + } 427 + elseif lsc == m.op then 428 + o = { 429 + kind = 'format'; 430 + style = 'strong'; 431 + spans = {buf}; 432 + } 433 + end 434 + if o then 435 + table.insert(spans, o) 436 + end 437 + end 438 + buf = '' 439 + lsc = 0 440 + end 441 + 442 + for c, p in ss.str.each(ctx.doc.enc, eqn) do 443 + local cl = ss.str.classify(ctx.doc.enc, c) 444 + local nc = 0 445 + if not cl.space then 446 + if cl.numeral then 447 + nc = m.num 448 + elseif cl.mathop or cl.symbol then 449 + nc = m.op 450 + elseif cl.letter then 451 + nc = m.var 452 + end 453 + if nc ~= lsc then 454 + flush() 455 + lsc = nc 456 + end 457 + buf = buf .. c 458 + end 459 + end 460 + flush() 461 + return spans 462 +end 463 + 464 +function ct.tool.namespace() 465 +-- some renderers need to be able to generate unique IDs for 466 +-- objects, including ones that users have not assigned IDs 467 +-- to, and objects with the same name in different unlabeled 468 +-- sections. to handle this, we provide a "namespace" mechanism, 469 +-- where some lua table (really its address in memory) is used 470 +-- as a handle for the object and a unique ID is attached to it. 471 +-- if the object has an ID of its own, it is guaranteed to be 472 +-- unique and returned; otherwise, a generic id of the form 473 +-- `x-%u` is generated, where %u is an integer that increments 474 +-- for every new object 405 475 local ids = {} 406 476 local canonicalID = {} 407 - local function getSafeID(obj,pfx) 477 + return function(obj,pfx) 408 478 pfx = pfx or '' 409 479 if canonicalID[obj] then 410 480 return canonicalID[obj] 411 481 elseif obj.id and ids[pfx .. obj.id] then 412 482 local objid = pfx .. obj.id 413 483 local newid 414 484 local i = 1 ................................................................................ 425 495 i = i + 1 until not ids[cid] 426 496 end 427 497 ids[cid] = obj 428 498 canonicalID[obj] = cid 429 499 return cid 430 500 end 431 501 end 432 - 433 - local footnotes = {} 434 - local footnotecount = 0 435 - 436 - local langsused = {} 437 - local langpairs = { 438 - lua = { color = 0x9377ff }; 439 - terra = { color = 0xff77c8 }; 440 - c = { name = 'C', color = 0x77ffe8 }; 441 - html = { color = 0xfff877 }; 442 - scheme = { color = 0x77ff88 }; 443 - lisp = { color = 0x77ff88 }; 444 - fortran = { color = 0xff779a }; 445 - python = { color = 0xffd277 }; 446 - ruby = { color = 0xcdd6ff }; 447 - } 448 - 449 - local stylesets = { 450 - footnote = [[ 451 - div.footnote { 452 - font-family: 90%; 453 - display: none; 454 - grid-template-columns: 1em 1fr min-content; 455 - grid-template-rows: 1fr min-content; 456 - position: fixed; 457 - padding: 1em; 458 - background: @tone(0.05); 459 - border: black; 460 - margin:auto; 461 - } 462 - div.footnote:target { display:grid; } 463 - @media screen { 464 - div.footnote { 465 - left: 10em; 466 - right: 10em; 467 - max-width: calc(@width + 2em); 468 - max-height: 30vw; 469 - bottom: 1em; 470 - } 471 - } 472 - @media print { 473 - div.footnote { 474 - position: relative; 475 - } 476 - div.footnote:first-of-type { 477 - border-top: 1px solid black; 478 - } 479 - } 480 - 481 - div.footnote > a[href="#0"]{ 482 - grid-row: 2/3; 483 - grid-column: 3/4; 484 - display: block; 485 - padding: 0.2em 0.7em; 486 - text-align: center; 487 - text-decoration: none; 488 - background: @tone(0.2); 489 - color: @tone(1); 490 - border: 1px solid black; 491 - margin-top: 0.6em; 492 - -webkit-user-select: none; 493 - -ms-user-select: none; 494 - user-select: none; 495 - -webkit-user-drag: none; 496 - user-drag: none; 497 - } 498 - div.footnote > a[href="#0"]:hover { 499 - background: @tone(0.3); 500 - color: @tone(2); 501 - } 502 - div.footnote > a[href="#0"]:active { 503 - background: @tone(0.05); 504 - color: @tone(0.4); 505 - } 506 - @media print { 507 - div.footnote > a[href="#0"]{ 508 - display:none; 509 - } 510 - } 511 - div.footnote > div.number { 512 - text-align:right; 513 - grid-row: 1/2; 514 - grid-column: 1/2; 515 - } 516 - div.footnote > div.text { 517 - grid-row: 1/2; 518 - grid-column: 2/4; 519 - padding-left: 1em; 520 - overflow-y: scroll; 521 - } 522 - ]]; 523 - header = [[ 524 - body { padding: 0 2.5em !important } 525 - h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); } 526 - h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; } 527 - h2 { font-size: 130%; margin: 0em -0.7em; } 528 - h3 { font-size: 110%; margin: 0em -0.5em; } 529 - h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; } 530 - h5 { font-size: 90%; font-weight: normal; } 531 - h6 { font-size: 80%; font-weight: normal; } 532 - h3, h4, h5, h6 { border-bottom-style: dotted !important; } 533 - h1,h2,h3,h4,h5,h6 { 534 - margin-top: 0; 535 - margin-bottom: 0; 536 - } 537 - :is(h1,h2,h3,h4,h5,h6) + p { 538 - margin-top: 0.4em; 539 - } 540 - 541 - ]]; 542 - headingAnchors = [[ 543 - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor { 544 - text-decoration: none; 545 - font-size: 1.2em; 546 - padding: 0.3em; 547 - opacity: 0%; 548 - transition: 0.3s; 549 - font-weight: 100; 550 - } 551 - :is(h1,h2,h3,h4,h5,h6):hover > a[href].anchor { 552 - opacity: 50%; 553 - } 554 - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor:hover { 555 - opacity: 100%; 556 - } 557 - 558 - ]] .. -- this is necessary to avoid the sections jumping around 559 - -- when focus changes from one to another 560 - [[ section { 561 - border: 1px solid transparent; 562 - } 563 - 564 - section:target { 565 - margin-left: -2em; 566 - margin-right: -2em; 567 - padding: 0 2em; 568 - background: @tone(0.04); 569 - border: 1px dotted @tone(0.3); 570 - } 571 - 572 - section:target > :is(h1,h2,h3,h4,h5,h6) { 573 - 574 - } 575 - ]]; 576 - paragraph = [[ 577 - p { 578 - margin: 0.7em 0; 579 - text-align: justify; 580 - } 581 - section { 582 - margin: 1.2em 0; 583 - } 584 - section:first-child { margin-top: 0; } 585 - ]]; 586 - accent = [[ 587 - @media screen { 588 - body { background: @bg; color: @fg } 589 - a[href] { 590 - color: @tone(0.7 30); 591 - text-decoration-color: @tone/0.4(0.7 30); 592 - } 593 - a[href]:hover { 594 - color: @tone(0.9 30); 595 - text-decoration-color: @tone/0.7(0.7 30); 596 - } 597 - h1 { color: @tone(2); } 598 - h2 { color: @tone(1.5); } 599 - h3 { color: @tone(1.2); } 600 - h4 { color: @tone(1); } 601 - h5,h6 { color: @tone(0.8); } 602 - } 603 - @media print { 604 - a[href] { 605 - text-decoration: none; 606 - color: black; 607 - font-weight: bold; 608 - } 609 - h1,h2,h3,h4,h5,h6 { 610 - border-bottom: 1px black; 611 - } 612 - } 613 - ]]; 614 - aside = [[ 615 - section > aside { 616 - text-align: justify; 617 - margin: 0 1.5em; 618 - padding: 0.5em 0.8em; 619 - background: @tone(0.05); 620 - font-size: 90%; 621 - border-left: 5px solid @tone(0.2 15); 622 - border-right: 5px solid @tone(0.2 15); 623 - } 624 - section > aside p { 625 - margin: 0; 626 - margin-top: 0.6em; 627 - } 628 - section > aside p:first-child { 629 - margin: 0; 630 - } 631 - ]]; 632 - code = [[ 633 - code { 634 - display: inline-block; 635 - background: @tone(0.9); 636 - color: @bg; 637 - font-family: monospace; 638 - font-size: 90%; 639 - padding: 3px 5px; 640 - } 641 - ]]; 642 - var = [[ 643 - var { 644 - font-style: italic; 645 - font-family: monospace; 646 - color: @tone(0.7); 647 - } 648 - code var { 649 - color: @tone(0.25); 650 - } 651 - ]]; 652 - math = [[ 653 - span.equation { 654 - display: inline-block; 655 - background: @tone(0.08); 656 - color: @tone(2); 657 - padding: 0.1em 0.3em; 658 - border: 1px solid @tone(0.5); 659 - } 660 - ]]; 661 - abbr = [[ 662 - abbr[title] { cursor: help; } 663 - ]]; 664 - editors_markup = [[]]; 665 - block_code_listing = [[ 666 - figure.listing { 667 - font-family: monospace; 668 - background: @tone(0.05); 669 - color: @fg; 670 - padding: 0; 671 - margin: 0.3em 0; 672 - counter-reset: line-number; 673 - position: relative; 674 - border: 1px solid @fg; 675 - } 676 - figure.listing>div { 677 - white-space: pre-wrap; 678 - tab-size: 3; 679 - -moz-tab-size: 3; 680 - counter-increment: line-number; 681 - text-indent: -2.3em; 682 - margin-left: 2.3em; 683 - } 684 - figure.listing>:is(div,hr)::before { 685 - width: 1.0em; 686 - padding: 0.2em 0.4em; 687 - text-align: right; 688 - display: inline-block; 689 - background-color: @tone(0.2); 690 - border-right: 1px solid @fg; 691 - content: counter(line-number); 692 - margin-right: 0.3em; 693 - } 694 - figure.listing>hr::before { 695 - color: transparent; 696 - padding-top: 0; 697 - padding-bottom: 0; 698 - } 699 - figure.listing>div::before { 700 - color: @fg; 701 - } 702 - figure.listing>div:last-child::before { 703 - padding-bottom: 0.5em; 704 - } 705 - figure.listing>figcaption:first-child { 706 - border: none; 707 - border-bottom: 1px solid @fg; 708 - } 709 - figure.listing>figcaption::after { 710 - display: block; 711 - float: right; 712 - font-weight: normal; 713 - font-style: italic; 714 - font-size: 70%; 715 - padding-top: 0.3em; 716 - } 717 - figure.listing>figcaption { 718 - font-family: sans-serif; 719 - font-size: 120%; 720 - padding: 0.2em 0.4em; 721 - border: none; 722 - color: @tone(2); 723 - } 724 - figure.listing > hr { 725 - border: none; 726 - margin: 0; 727 - height: 0.7em; 728 - counter-increment: line-number; 729 - } 730 - ]]; 731 - } 732 - 733 - local stylesNeeded = {} 734 - 735 - local render_state_handle = { 736 - doc = doc; 737 - opts = opts; 738 - style_rules = styles; -- use stylesneeded if at all possible 739 - stylesets = stylesets; 740 - stylesets_active = stylesNeeded; 741 - obj_htmlid = getSafeID; 742 - -- remaining fields added later 743 - } 744 - 745 - local renderJob = doc:job('render_html', nil, render_state_handle) 746 - doc.stage.job = renderJob; 747 - 748 - local runhook = function(h, ...) 749 - return renderJob:hook(h, render_state_handle, ...) 750 - end 751 - 752 - local tagproc do 753 - local elt = function(t,attrs) 754 - return f('<%s%s>', t, 755 - attrs and ss.reduce(function(a,b) return a..b end, '', 756 - ss.map(function(v,k) 757 - if v == true 758 - then return ' '..k 759 - elseif v then return f(' %s="%s"', k, v) 760 - end 761 - end, attrs)) or '') 762 - end 763 - 764 - tagproc = { 765 - toTXT = { 766 - tag = function(t,a,v) return v end; 767 - elt = function(t,a) return '' end; 768 - catenate = table.concat; 769 - }; 770 - toIR = { 771 - tag = function(t,a,v,o) return { 772 - tag = t, attrs = a; 773 - nodes = type(v) == 'string' and {v} or v, src = o 774 - } end; 775 - 776 - elt = function(t,a,o) return { 777 - tag = t, attrs = a, src = o 778 - } end; 779 - 780 - catenate = function(...) return ... end; 781 - }; 782 - toHTML = { 783 - elt = elt; 784 - tag = function(t,attrs,body) 785 - return f('%s%s</%s>', elt(t,attrs), body, t) 786 - end; 787 - catenate = table.concat; 788 - }; 789 - } 790 - end 791 - 792 - local function getBaseRenderers(procs, span_renderers) 793 - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 794 - local htmlDoc = function(title, head, body) 795 - return [[<!doctype html>]] .. tag('html',nil, 796 - tag('head', nil, 797 - elt('meta',{charset = 'utf-8'}) .. 798 - (title and tag('title', nil, title) or '') .. 799 - (head or '')) .. 800 - tag('body', nil, body or '')) 801 - end 802 - 803 - local function htmlSpan(spans, block, sec) 804 - local text = {} 805 - for k,v in pairs(spans) do 806 - if type(v) == 'string' then 807 - v=v:gsub('[<>&"]', function(x) 808 - return string.format('&#%02u;', string.byte(x)) 809 - end) 810 - for fn, ext in renderJob:each('hook','render_html_sanitize') do 811 - v = fn(renderJob:delegate(ext), v) 812 - end 813 - table.insert(text,v) 814 - else 815 - table.insert(text, (span_renderers[v.kind](v, block, sec))) 816 - end 817 - end 818 - return table.concat(text) 819 - end 820 - return {htmlDoc=htmlDoc, htmlSpan=htmlSpan} 821 - end 822 - 823 - local spanparse = function(...) 824 - local s = ct.parse_span(...) 825 - doc.docjob:hook('meddle_span', s) 826 - return s 827 - end 828 - 829 - local cssRulesFor = {} 830 - local function getSpanRenderers(procs) 831 - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 832 - local span_renderers = {} 833 - local plainrdr = getBaseRenderers(tagproc.toTXT, span_renderers) 834 - local htmlSpan = getBaseRenderers(procs, span_renderers).htmlSpan 835 - 836 - function span_renderers.format(sp,...) 837 - local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'} 838 - if sp.style == 'literal' and not opts['fossil-uv'] then 839 - stylesNeeded.code = true 840 - elseif sp.style == 'strike' or sp.style == 'insert' then 841 - stylesNeeded.editors_markup = true 842 - elseif sp.style == 'variable' then 843 - stylesNeeded.var = true 844 - end 845 - return tag(tags[sp.style],nil,htmlSpan(sp.spans,...)) 846 - end 847 - 848 - function span_renderers.deref(t,b,s) 849 - local r = b.origin:ref(t.ref) 850 - local name = t.ref 851 - if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end 852 - if type(r) == 'string' then 853 - stylesNeeded.abbr = true 854 - return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name) 855 - end 856 - if r.kind == 'resource' then 857 - local rid = getSafeID(r, 'res-') 858 - if r.class == 'image' then 859 - if not cssRulesFor[r] then 860 - local css = prepcss(string.format([[ 861 - section p > .%s { 862 - } 863 - ]], rid)) 864 - stylesets[r] = css 865 - cssRulesFor[r] = css 866 - stylesNeeded[r] = true 867 - end 868 - return tag('div',{class=rid},catenate{'blaah'}) 869 - elseif r.class == 'video' then 870 - local vid = {} 871 - return tag('video',nil,vid) 872 - elseif r.class == 'font' then 873 - b.origin:fail('fonts cannot be instantiated, use %font directive instead') 874 - end 875 - else 876 - b.origin:fail('%s is not an object that can be embedded', t.ref) 877 - end 878 - end 879 - 880 - function span_renderers.var(v,b,s) 881 - local val 882 - if v.pos then 883 - if not v.origin.invocation then 884 - v.origin:fail 'positional arguments can only be used in a macro invocation' 885 - elseif not v.origin.invocation.args[v.pos] then 886 - v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) 887 - end 888 - val = v.origin.invocation.args[v.pos] 889 - else 890 - val = v.origin.doc:context_var(v.var, v.origin) 891 - end 892 - if v.raw then 893 - return val 894 - else 895 - return htmlSpan(ct.parse_span(val, v.origin), b, s) 896 - end 897 - end 898 - 899 - function span_renderers.raw(v,b,s) 900 - return htmlSpan(v.spans, b, s) 901 - end 902 - 903 - function span_renderers.link(sp,b,s) 904 - local href 905 - if b.origin.doc.sections[sp.ref] then 906 - href = '#' .. sp.ref 907 - else 908 - if sp.addr then href = sp.addr else 909 - local r = b.origin:ref(sp.ref) 910 - if type(r) == 'table' then 911 - href = '#' .. getSafeID(r) 912 - else href = r end 913 - end 914 - end 915 - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href) 916 - end 917 - 918 - span_renderers['line-break'] = function(sp,b,s) 919 - return elt('br') 920 - end 921 - 922 - function span_renderers.macro(m,b,s) 923 - local macroname = plainrdr.htmlSpan( 924 - ct.parse_span(m.macro, b.origin), b,s) 925 - local r = b.origin:ref(macroname) 926 - if type(r) ~= 'string' then 927 - b.origin:fail('%s is an object, not a reference', t.ref) 928 - end 929 - local mctx = b.origin:clone() 930 - mctx.invocation = m 931 - return htmlSpan(ct.parse_span(r, mctx),b,s) 932 - end 933 - function span_renderers.math(m,b,s) 934 - stylesNeeded.math = true 935 - return tag('span',{class='equation'},htmlSpan(m.spans, b, s)) 936 - end; 937 - function span_renderers.directive(d,b,s) 938 - if d.ext == 'html' then 939 - elseif b.origin.doc:allow_ext(d.ext) then 940 - elseif d.crit then 941 - b.origin:fail('critical extension %s unavailable', d.ext) 942 - elseif d.failthru then 943 - return htmlSpan(d.spans, b, s) 944 - end 945 - end 946 - function span_renderers.footnote(f,b,s) 947 - stylesNeeded.footnote = true 948 - local source, sid, ssec = b.origin:ref(f.ref) 949 - local cnc = getSafeID(ssec) .. ' ' .. sid 950 - local fn 951 - if footnotes[cnc] then 952 - fn = footnotes[cnc] 953 - else 954 - footnotecount = footnotecount + 1 955 - fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source} 956 - fn.id = getSafeID(fn) 957 - footnotes[cnc] = fn 958 - end 959 - return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) .. 960 - tag('sup',nil, fn.num)) 961 - end 962 - 963 - return span_renderers 964 - end 965 - 966 - local function getBlockRenderers(procs, sr) 967 - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 968 - local null = function() return catenate{} end 969 - 970 - local block_renderers = { 971 - anchor = function(b,s) 972 - return tag('a',{id = getSafeID(b)},null()) 973 - end; 974 - paragraph = function(b,s) 975 - stylesNeeded.paragraph = true; 976 - return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) 977 - end; 978 - directive = function(b,s) 979 - -- deal with renderer directives 980 - local _, cmd, args = b.words(2) 981 - if cmd == 'page-title' then 982 - if not opts.title then doctitle = args end 983 - elseif b.critical then 984 - b.origin:fail('critical HTML renderer directive “%s” not supported', cmd) 985 - end 986 - end; 987 - label = function(b,s) 988 - if ct.sec.is(b.captions) then 989 - if not (opts['fossil-uv'] or opts.snippet) then 990 - stylesNeeded.header = true 991 - end 992 - local h = math.min(6,math.max(1,b.captions.depth)) 993 - return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b) 994 - else 995 - -- handle other uses of labels here 996 - end 997 - end; 998 - ['list-item'] = function(b,s) 999 - return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) 1000 - end; 1001 - table = function(b,s) 1002 - local tb = {} 1003 - for i, r in ipairs(b.rows) do 1004 - local row = {} 1005 - for i, c in ipairs(r) do 1006 - table.insert(row, tag(c.header and 'th' or 'td', 1007 - {align=c.align}, sr.htmlSpan(c.spans, b))) 1008 - end 1009 - table.insert(tb, tag('tr',nil,catenate(row))) 1010 - end 1011 - return tag('table',nil,catenate(tb)) 1012 - end; 1013 - listing = function(b,s) 1014 - stylesNeeded.block_code_listing = true 1015 - local nodes = ss.map(function(l) 1016 - if #l > 0 then 1017 - return tag('div',nil,sr.htmlSpan(l, b, s)) 1018 - else 1019 - return elt('hr') 1020 - end 1021 - end, b.lines) 1022 - if b.title then 1023 - table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title))) 1024 - end 1025 - if b.lang then langsused[b.lang] = true end 1026 - return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) 1027 - end; 1028 - aside = function(b,s) 1029 - local bn = {} 1030 - stylesNeeded.aside = true 1031 - if #b.lines == 1 then 1032 - bn[1] = sr.htmlSpan(b.lines[1], b, s) 1033 - else 1034 - for _,v in pairs(b.lines) do 1035 - table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s))) 1036 - end 1037 - end 1038 - return tag('aside', {}, bn) 1039 - end; 1040 - ['break'] = function() -- HACK 1041 - -- lists need to be rewritten to work like asides 1042 - return ''; 1043 - end; 1044 - } 1045 - return block_renderers; 1046 - end 1047 - 1048 - local function getRenderers(procs) 1049 - local span_renderers = getSpanRenderers(procs) 1050 - local r = getBaseRenderers(procs,span_renderers) 1051 - r.block_renderers = getBlockRenderers(procs, r) 1052 - return r 1053 - end 1054 - 1055 - local astproc = { 1056 - toHTML = getRenderers(tagproc.toHTML); 1057 - toTXT = getRenderers(tagproc.toTXT); 1058 - toIR = { }; 1059 - } 1060 - astproc.toIR.span_renderers = ss.clone(astproc.toHTML); 1061 - astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML); 1062 - -- note we use HTML here instead of IR span renderers, because as things 1063 - -- currently stand we don't need that level of resolution. if we ever 1064 - -- get to the point where we want to be able to twiddle spans around 1065 - -- we'll need to introduce an IR span renderer 1066 - 1067 - render_state_handle.astproc = astproc; 1068 - render_state_handle.tagproc = tagproc; 1069 - 1070 - -- bind to legacy names 1071 - -- yikes this needs to be cleaned up so badly 1072 - local ir = {} 1073 - local dr = astproc.toHTML -- default renderers 1074 - local plainr = astproc.toTXT 1075 - local irBlockRdrs = astproc.toIR.block_renderers; 1076 - 1077 - render_state_handle.ir = ir; 1078 - 1079 - local function renderBlocks(blocks, irs) 1080 - for i, block in ipairs(blocks) do 1081 - local rd 1082 - if irBlockRdrs[block.kind] then 1083 - rd = irBlockRdrs[block.kind](block,sec) 1084 - else 1085 - local rdr = renderJob:proc('render',block.kind,'html') 1086 - if rdr then 1087 - rd = rdr({ 1088 - state = render_state_handle; 1089 - tagproc = tagproc.toIR; 1090 - astproc = astproc.toIR; 1091 - }, block, sec) 1092 - end 1093 - end 1094 - if rd then 1095 - if opts['heading-anchors'] and block == sec.heading_node then 1096 - stylesNeeded.headingAnchors = true 1097 - table.insert(rd.nodes, ' ') 1098 - table.insert(rd.nodes, { 1099 - tag = 'a'; 1100 - attrs = {href = '#' .. irs.attrs.id, class='anchor'}; 1101 - nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'}; 1102 - }) 1103 - end 1104 - if rd.src and rd.src.origin.lang then 1105 - if not rd.attrs then rd.attrs = {} end 1106 - rd.attrs.lang = rd.src.origin.lang 1107 - end 1108 - table.insert(irs.nodes, rd) 1109 - runhook('ir_section_node_insert', rd, irs, sec) 1110 - end 1111 - end 1112 - end 1113 - runhook('ir_assemble', ir) 1114 - for i, sec in ipairs(doc.secorder) do 1115 - if doctitle == nil and sec.depth == 1 and sec.heading_node then 1116 - doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec) 1117 - end 1118 - local irs 1119 - if sec.kind == 'ordinary' then 1120 - if #(sec.blocks) > 0 then 1121 - irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}} 1122 - runhook('ir_section_build', irs, sec) 1123 - renderBlocks(sec.blocks, irs) 1124 - end 1125 - elseif sec.kind == 'blockquote' then 1126 - elseif sec.kind == 'listing' then 1127 - elseif sec.kind == 'embed' then 1128 - end 1129 - if irs then table.insert(ir, irs) end 1130 - end 1131 - 1132 - for _, fn in pairs(footnotes) do 1133 - local tag = tagproc.toIR.tag 1134 - local body = {nodes={}} 1135 - local ftir = {} 1136 - for l in fn.source:gmatch('([^\n]*)') do 1137 - ct.parse_line(l, fn.origin, ftir) 1138 - end 1139 - renderBlocks(ftir,body) 1140 - local note = tag('div',{class='footnote',id=fn.id}, { 1141 - tag('div',{class='number'}, tostring(fn.num)), 1142 - tag('div',{class='text'}, body.nodes), 1143 - tag('a',{href='#0'},'close') 1144 - }) 1145 - table.insert(ir, note) 1146 - end 1147 - 1148 - -- restructure passes 1149 - runhook('ir_restructure_pre', ir) 1150 - 1151 - ---- list insertion pass 1152 - local lists = {} 1153 - for _, sec in pairs(ir) do 1154 - if sec.tag == 'section' then 1155 - local i = 1 while i <= #sec.nodes do local v = sec.nodes[i] 1156 - if v.tag == 'li' then 1157 - local ltag 1158 - if v.src.ordered 1159 - then ltag = 'ol' 1160 - else ltag = 'ul' 1161 - end 1162 - local last = i>1 and sec.nodes[i-1] 1163 - if last and last.embed == 'list' and not ( 1164 - last.ref[#last.ref].src.depth == v.src.depth and 1165 - last.ref[#last.ref].src.ordered ~= v.src.ordered 1166 - ) then 1167 - -- add to existing list 1168 - table.insert(last.ref, v) 1169 - table.remove(sec.nodes, i) i = i - 1 1170 - else 1171 - -- wrap in list 1172 - local newls = {v} 1173 - sec.nodes[i] = {embed = 'list', ref = newls} 1174 - table.insert(lists,newls) 1175 - end 1176 - end 1177 - i = i + 1 end 1178 - end 1179 - end 1180 - 1181 - for _, sec in pairs(ir) do 1182 - if sec.tag == 'section' then 1183 - for i, elt in pairs(sec.nodes) do 1184 - if elt.embed == 'list' then 1185 - local function fail_nest() 1186 - elt.ref[1].src.origin:fail('improper list nesting') 1187 - end 1188 - local struc = {attrs={}, nodes={}} 1189 - if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end 1190 - if elt.ref[1].src.depth ~= 1 then fail_nest() end 1191 - 1192 - local stack = {struc} 1193 - local copyNodes = function(old,new) 1194 - for i,v in ipairs(old) do new[#new + i] = v end 1195 - end 1196 - for i,e in ipairs(elt.ref) do 1197 - if e.src.depth > #stack then 1198 - if e.src.depth - #stack > 1 then fail_nest() end 1199 - local newls = {attrs={}, nodes={e}} 1200 - copyNodes(e.nodes,newls) 1201 - if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end 1202 - table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls) 1203 - table.insert(stack, newls) 1204 - else 1205 - if e.src.depth < #stack then 1206 - -- pop entries off the stack 1207 - for i=#stack, e.src.depth+1, -1 do stack[i] = nil end 1208 - end 1209 - table.insert(stack[#stack].nodes, e) 1210 - end 1211 - end 1212 - 1213 - sec.nodes[i] = struc 1214 - end 1215 - end 1216 - end 1217 - end 1218 - 1219 - runhook('ir_restructure_post', ir) 1220 - 1221 - -- collection pass 1222 - local function collect_nodes(t) 1223 - local ts = '' 1224 - for i,v in ipairs(t) do 1225 - if type(v) == 'string' then 1226 - ts = ts .. v 1227 - elseif v.nodes then 1228 - ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes)) 1229 - elseif v.text then 1230 - ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text) 1231 - else 1232 - ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs) 1233 - end 1234 - end 1235 - return ts 1236 - end 1237 - local body = collect_nodes(ir) 1238 - 1239 - for k in pairs(langsused) do 1240 - local spec = langpairs[k] or {color=0xaaaaaa} 1241 - stylesets.block_code_listing = stylesets.block_code_listing .. string.format( 1242 - [[section > figure.listing[lang="%s"]>figcaption::after 1243 - { content: '%s'; color: #%06x }]], 1244 - k, spec.name or k, spec.color) 1245 - end 1246 - 1247 - local prepcss = function(css) 1248 - local tone = function(fac, sat, sep, alpha) 1249 - local hsl = function(h,s,l,a) 1250 - local v = string.format('%s, %u%%, %u%%', h,s,l) 1251 - if a then 1252 - return string.format('hsla(%s, %s)', v,a) 1253 - else 1254 - return string.format('hsl(%s)', v) 1255 - end 1256 - end 1257 - sat = sat or 1 1258 - fac = math.max(math.min(fac, 1), 0) 1259 - sat = math.max(math.min(sat, 1), 0) 1260 - if opts.accent then 1261 - local hue = 'var(--accent)' 1262 - local hsep = tonumber(opts['hue-spread']) 1263 - if hsep and sep and sep ~= 0 then 1264 - hue = string.format('calc(%s - %s)', hue, sep * hsep) 1265 - end 1266 - return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha) 1267 - else 1268 - local g = math.floor(0xFF * fac) 1269 - return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha)) 1270 - end 1271 - end 1272 - local replace = function(var,alpha,param) 1273 - local tonespan = opts.accent and .1 or 0 1274 - local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan 1275 - local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan 1276 - if var == 'bg' then 1277 - return tone(tbg,nil,nil,tonumber(alpha)) 1278 - elseif var == 'fg' then 1279 - return tone(tfg,nil,nil,tonumber(alpha)) 1280 - elseif var == 'width' then 1281 - return opts['width'] or '100vw' 1282 - elseif var == 'tone' then 1283 - local l, sep, sat 1284 - for i=1,3 do -- 🙄 1285 - l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$') 1286 - if l then break end 1287 - end 1288 - l = ss.math.lerp(tonumber(l), tbg, tfg) 1289 - return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha)) 1290 - end 1291 - end 1292 - css = css:gsub('@(%b[]):(%b[])', function(v,d) return opts[v:sub(2,-2)] or v:sub(2,-2) end) 1293 - css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace) 1294 - css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end) 1295 - css = css:gsub('@(%w+)/([0-9.]+)', replace) 1296 - css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) 1297 - return (css:gsub('%s+',' ')) 1298 - end 1299 - 1300 - local styles = {} 1301 - if opts.width then 1302 - table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width)) 1303 - end 1304 - if opts.accent then 1305 - table.insert(styles, string.format(':root {--accent:%s}', opts.accent)) 1306 - end 1307 - if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then 1308 - stylesNeeded.accent = true 1309 - end 1310 - 1311 - 1312 - for k in pairs(stylesNeeded) do 1313 - if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end 1314 - table.insert(styles, prepcss(stylesets[k])) 1315 - end 1316 - 1317 - local head = {} 1318 - local styletag = '' 1319 - if opts['link-css'] then 1320 - local css = opts['link-css'] 1321 - if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end 1322 - styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']}) 1323 - end 1324 - if next(styles) then 1325 - if opts['gen-styles'] then 1326 - styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles)) 1327 - end 1328 - table.insert(head, styletag) 1329 - end 1330 - 1331 - if opts['fossil-uv'] then 1332 - return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body) 1333 - elseif opts.snippet then 1334 - return styletag .. body 1335 - else 1336 - return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) 1337 - end 1338 502 end 503 + 504 +-- renderer engines 1339 505 1340 506 do -- define span control sequences 1341 507 local function formatter(sty) 1342 508 return function(s,c) 1343 509 return { 1344 510 kind = 'format'; 1345 511 style = sty; ................................................................................ 1474 640 end 1475 641 return r 1476 642 end) 1477 643 local m = {s} --TODO 1478 644 return { 1479 645 kind = 'math'; 1480 646 original = s; 1481 - spans = m; 647 + spans = {s}; 1482 648 origin = c:clone(); 1483 649 }; 1484 650 end}; 1485 651 {seq = '&', parse = function(s, c) 1486 652 local r, t = s:match '^([^%s]+)%s*(.-)$' 1487 653 return { 1488 654 kind = 'deref'; 1489 655 spans = (t and t ~= "") and ct.parse_span(t, c) or {}; 1490 - ref = r; 656 + ref = r; 1491 657 origin = c:clone(); 1492 658 } 1493 659 end}; 1494 660 {seq = '^', parse = function(s, c) 1495 661 local fn, t = s:match '^([^%s]+)%s*(.-)$' 1496 662 return { 1497 663 kind = 'footnote'; ................................................................................ 1576 742 table.insert(spans,o) 1577 743 elseif c == '[' then 1578 744 flush() 1579 745 local substr, following = delimited('[',']',str:sub(p.byte)) 1580 746 p.next.byte = following + p.byte 1581 747 local found = false 1582 748 for _,i in pairs(ct.spanctls) do 1583 - if startswith(substr, i.seq) then 749 + if ss.str.begins(substr, i.seq) then 1584 750 found = true 1585 751 table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx)) 1586 752 break 1587 753 end 1588 754 end 1589 755 if not found then 1590 756 ctx:fail('no recognized control sequence in [%s]', substr)
Modified makefile from [4482353657] to [ac8c1379b3].
1 +# [ʞ] makefile 2 +# ~ lexi hale <lexi@hale.su> 3 +# 🄯 AGPLv3 4 +# ? this script performs the tasks necessary to produce a mostly 5 +# standalone cortav executable from the source files in the 6 +# repository. it assumes the presence of the following tools 7 +# in $SHELL or in $PATH: 8 +# 9 +# * which * cat 10 +# * mkdir * echo 11 +# * install * lua 12 +# * luac * sh 13 +# 14 +# if any are not present, the build will fail, although a missing 15 +# `which` can be worked around by specifying the paths to lua, luac, 16 +# and `sh` directly 17 +# 18 +# eventually you will be able to set a "standalone" variable to 19 +# create a truly standalone binary, by embedding the binary in a 20 +# C program and statically linking it to lua. 21 + 1 22 lua != which lua 2 23 luac != which luac 3 24 sh != which sh 4 25 5 26 extens = $(wildcard ext/*.lua) 6 27 extens-names ?= $(basename $(notdir $(extens))) 28 +rendrs = $(wildcard render/*.lua) 29 +rendrs-names ?= $(basename $(notdir $(rendrs))) 30 + 7 31 build = build 8 32 executable = cortav 9 33 default-format-flags = -m html:width 40em 10 34 11 35 prefix = $(HOME)/.local 12 36 bin-prefix = $(prefix)/bin 13 37 share-prefix = $(prefix)/share/$(executable) ................................................................................ 18 42 # this is not necessary for parsing the format, and can be 19 43 # disabled by blanking the encoding-data list when building 20 44 # ($ make encoding-data=) 21 45 encoding-data = ucstbls 22 46 encoding-files = $(patsubst %,$(build)/%.lc,$(encoding-data)) 23 47 encoding-data-ucs = https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt 24 48 25 -$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(extens) cli.lua | $(build)/ 49 +$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(rendrs) $(extens) cli.lua | $(build)/ 26 50 @echo ' » building with extensions $(extens-names)' 51 + @echo ' » building with renderers $(rendrs-names)' 27 52 echo '#!$(lua)' > $@ 28 53 luac -o - $^ >> $@ 29 54 chmod +x $@ 30 55 31 56 $(build)/cortav.html: cortav.ct $(build)/$(executable) | $(build)/ 32 57 $(build)/$(executable) $< -o $@ -m render:format html -y html:fossil-uv 33 58 ................................................................................ 52 77 echo "Exec=$(bin-prefix)/cortav-view.sh" >>$@ 53 78 54 79 %/: 55 80 mkdir -p $@ 56 81 57 82 $(build)/unicode.txt: | $(build)/ 58 83 curl $(encoding-data-ucs) > $@ 59 -$(build)/ucstbls.lc: $(build)/unicode.txt | $(build)/ 84 +$(build)/ucstbls.lc: $(build)/unicode.txt tools/ucs.lua | $(build)/ 60 85 $(lua) tools/ucs.lua $< | $(luac) -o $@ - 61 86 62 87 .PHONY: install 63 88 install: $(build)/cortav $(build)/cortav-view.sh $(build)/velartrill-cortav-view.desktop | $(bin-prefix)/ 64 89 install $(build)/$(executable) $(bin-prefix) 65 90 install $(build)/cortav-view.sh $(bin-prefix) 66 91 xdg-mime install desk/velartrill-cortav.xml
Added render/html.lua version [1e64ee70c7].
1 +local ct = require 'cortav' 2 +local ss = require 'sirsem' 3 + 4 +-- install rendering function for html 5 +function ct.render.html(doc, opts) 6 + local doctitle = opts['title'] 7 + local f = string.format 8 + local getSafeID = ct.tool.namespace() 9 + 10 + local footnotes = {} 11 + local footnotecount = 0 12 + 13 + local langsused = {} 14 + local langpairs = { 15 + lua = { color = 0x9377ff }; 16 + terra = { color = 0xff77c8 }; 17 + c = { name = 'C', color = 0x77ffe8 }; 18 + html = { color = 0xfff877 }; 19 + scheme = { color = 0x77ff88 }; 20 + lisp = { color = 0x77ff88 }; 21 + fortran = { color = 0xff779a }; 22 + python = { color = 0xffd277 }; 23 + ruby = { color = 0xcdd6ff }; 24 + } 25 + 26 + local stylesets = { 27 + footnote = [[ 28 + div.footnote { 29 + font-family: 90%; 30 + display: none; 31 + grid-template-columns: 1em 1fr min-content; 32 + grid-template-rows: 1fr min-content; 33 + position: fixed; 34 + padding: 1em; 35 + background: @tone(0.05); 36 + border: black; 37 + margin:auto; 38 + } 39 + div.footnote:target { display:grid; } 40 + @media screen { 41 + div.footnote { 42 + left: 10em; 43 + right: 10em; 44 + max-width: calc(@width + 2em); 45 + max-height: 30vw; 46 + bottom: 1em; 47 + } 48 + } 49 + @media print { 50 + div.footnote { 51 + position: relative; 52 + } 53 + div.footnote:first-of-type { 54 + border-top: 1px solid black; 55 + } 56 + } 57 + 58 + div.footnote > a[href="#0"]{ 59 + grid-row: 2/3; 60 + grid-column: 3/4; 61 + display: block; 62 + padding: 0.2em 0.7em; 63 + text-align: center; 64 + text-decoration: none; 65 + background: @tone(0.2); 66 + color: @tone(1); 67 + border: 1px solid black; 68 + margin-top: 0.6em; 69 + -webkit-user-select: none; 70 + -ms-user-select: none; 71 + user-select: none; 72 + -webkit-user-drag: none; 73 + user-drag: none; 74 + } 75 + div.footnote > a[href="#0"]:hover { 76 + background: @tone(0.3); 77 + color: @tone(2); 78 + } 79 + div.footnote > a[href="#0"]:active { 80 + background: @tone(0.05); 81 + color: @tone(0.4); 82 + } 83 + @media print { 84 + div.footnote > a[href="#0"]{ 85 + display:none; 86 + } 87 + } 88 + div.footnote > div.number { 89 + text-align:right; 90 + grid-row: 1/2; 91 + grid-column: 1/2; 92 + } 93 + div.footnote > div.text { 94 + grid-row: 1/2; 95 + grid-column: 2/4; 96 + padding-left: 1em; 97 + overflow-y: scroll; 98 + } 99 + ]]; 100 + header = [[ 101 + body { padding: 0 2.5em !important } 102 + h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); } 103 + h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; } 104 + h2 { font-size: 130%; margin: 0em -0.7em; } 105 + h3 { font-size: 110%; margin: 0em -0.5em; } 106 + h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; } 107 + h5 { font-size: 90%; font-weight: normal; } 108 + h6 { font-size: 80%; font-weight: normal; } 109 + h3, h4, h5, h6 { border-bottom-style: dotted !important; } 110 + h1,h2,h3,h4,h5,h6 { 111 + margin-top: 0; 112 + margin-bottom: 0; 113 + } 114 + :is(h1,h2,h3,h4,h5,h6) + p { 115 + margin-top: 0.4em; 116 + } 117 + 118 + ]]; 119 + headingAnchors = [[ 120 + :is(h1,h2,h3,h4,h5,h6) > a[href].anchor { 121 + text-decoration: none; 122 + font-size: 1.2em; 123 + padding: 0.3em; 124 + opacity: 0%; 125 + transition: 0.3s; 126 + font-weight: 100; 127 + } 128 + :is(h1,h2,h3,h4,h5,h6):hover > a[href].anchor { 129 + opacity: 50%; 130 + } 131 + :is(h1,h2,h3,h4,h5,h6) > a[href].anchor:hover { 132 + opacity: 100%; 133 + } 134 + 135 + ]] .. -- this is necessary to avoid the sections jumping around 136 + -- when focus changes from one to another 137 + [[ section { 138 + border: 1px solid transparent; 139 + } 140 + 141 + section:target { 142 + margin-left: -2em; 143 + margin-right: -2em; 144 + padding: 0 2em; 145 + background: @tone(0.04); 146 + border: 1px dotted @tone(0.3); 147 + } 148 + 149 + section:target > :is(h1,h2,h3,h4,h5,h6) { 150 + 151 + } 152 + ]]; 153 + paragraph = [[ 154 + p { 155 + margin: 0.7em 0; 156 + text-align: justify; 157 + } 158 + section { 159 + margin: 1.2em 0; 160 + } 161 + section:first-child { margin-top: 0; } 162 + ]]; 163 + accent = [[ 164 + @media screen { 165 + body { background: @bg; color: @fg } 166 + a[href] { 167 + color: @tone(0.7 30); 168 + text-decoration-color: @tone/0.4(0.7 30); 169 + } 170 + a[href]:hover { 171 + color: @tone(0.9 30); 172 + text-decoration-color: @tone/0.7(0.7 30); 173 + } 174 + h1 { color: @tone(2); } 175 + h2 { color: @tone(1.5); } 176 + h3 { color: @tone(1.2); } 177 + h4 { color: @tone(1); } 178 + h5,h6 { color: @tone(0.8); } 179 + } 180 + @media print { 181 + a[href] { 182 + text-decoration: none; 183 + color: black; 184 + font-weight: bold; 185 + } 186 + h1,h2,h3,h4,h5,h6 { 187 + border-bottom: 1px black; 188 + } 189 + } 190 + ]]; 191 + aside = [[ 192 + section > aside { 193 + text-align: justify; 194 + margin: 0 1.5em; 195 + padding: 0.5em 0.8em; 196 + background: @tone(0.05); 197 + font-size: 90%; 198 + border-left: 5px solid @tone(0.2 15); 199 + border-right: 5px solid @tone(0.2 15); 200 + } 201 + section > aside p { 202 + margin: 0; 203 + margin-top: 0.6em; 204 + } 205 + section > aside p:first-child { 206 + margin: 0; 207 + } 208 + ]]; 209 + code = [[ 210 + code { 211 + display: inline-block; 212 + background: @tone(0.9); 213 + color: @bg; 214 + font-family: monospace; 215 + font-size: 90%; 216 + padding: 3px 5px; 217 + } 218 + ]]; 219 + var = [[ 220 + var { 221 + font-style: italic; 222 + font-family: monospace; 223 + color: @tone(0.7); 224 + } 225 + code var { 226 + color: @tone(0.25); 227 + } 228 + ]]; 229 + math = [[ 230 + span.equation { 231 + display: inline-block; 232 + background: @tone(0.08); 233 + color: @tone(2); 234 + padding: 0.1em 0.3em; 235 + border: 1px solid @tone(0.5); 236 + } 237 + ]]; 238 + abbr = [[ 239 + abbr[title] { cursor: help; } 240 + ]]; 241 + editors_markup = [[]]; 242 + block_code_listing = [[ 243 + figure.listing { 244 + font-family: monospace; 245 + background: @tone(0.05); 246 + color: @fg; 247 + padding: 0; 248 + margin: 0.3em 0; 249 + counter-reset: line-number; 250 + position: relative; 251 + border: 1px solid @fg; 252 + } 253 + figure.listing>div { 254 + white-space: pre-wrap; 255 + tab-size: 3; 256 + -moz-tab-size: 3; 257 + counter-increment: line-number; 258 + text-indent: -2.3em; 259 + margin-left: 2.3em; 260 + } 261 + figure.listing>:is(div,hr)::before { 262 + width: 1.0em; 263 + padding: 0.2em 0.4em; 264 + text-align: right; 265 + display: inline-block; 266 + background-color: @tone(0.2); 267 + border-right: 1px solid @fg; 268 + content: counter(line-number); 269 + margin-right: 0.3em; 270 + } 271 + figure.listing>hr::before { 272 + color: transparent; 273 + padding-top: 0; 274 + padding-bottom: 0; 275 + } 276 + figure.listing>div::before { 277 + color: @fg; 278 + } 279 + figure.listing>div:last-child::before { 280 + padding-bottom: 0.5em; 281 + } 282 + figure.listing>figcaption:first-child { 283 + border: none; 284 + border-bottom: 1px solid @fg; 285 + } 286 + figure.listing>figcaption::after { 287 + display: block; 288 + float: right; 289 + font-weight: normal; 290 + font-style: italic; 291 + font-size: 70%; 292 + padding-top: 0.3em; 293 + } 294 + figure.listing>figcaption { 295 + font-family: sans-serif; 296 + font-size: 120%; 297 + padding: 0.2em 0.4em; 298 + border: none; 299 + color: @tone(2); 300 + } 301 + figure.listing > hr { 302 + border: none; 303 + margin: 0; 304 + height: 0.7em; 305 + counter-increment: line-number; 306 + } 307 + ]]; 308 + } 309 + 310 + local stylesNeeded = {} 311 + 312 + local render_state_handle = { 313 + doc = doc; 314 + opts = opts; 315 + style_rules = styles; -- use stylesneeded if at all possible 316 + stylesets = stylesets; 317 + stylesets_active = stylesNeeded; 318 + obj_htmlid = getSafeID; 319 + -- remaining fields added later 320 + } 321 + 322 + local renderJob = doc:job('render_html', nil, render_state_handle) 323 + doc.stage.job = renderJob; 324 + 325 + local runhook = function(h, ...) 326 + return renderJob:hook(h, render_state_handle, ...) 327 + end 328 + 329 + local tagproc do 330 + local elt = function(t,attrs) 331 + return f('<%s%s>', t, 332 + attrs and ss.reduce(function(a,b) return a..b end, '', 333 + ss.map(function(v,k) 334 + if v == true 335 + then return ' '..k 336 + elseif v then return f(' %s="%s"', k, v) 337 + end 338 + end, attrs)) or '') 339 + end 340 + 341 + tagproc = { 342 + toTXT = { 343 + tag = function(t,a,v) return v end; 344 + elt = function(t,a) return '' end; 345 + catenate = table.concat; 346 + }; 347 + toIR = { 348 + tag = function(t,a,v,o) return { 349 + tag = t, attrs = a; 350 + nodes = type(v) == 'string' and {v} or v, src = o 351 + } end; 352 + 353 + elt = function(t,a,o) return { 354 + tag = t, attrs = a, src = o 355 + } end; 356 + 357 + catenate = function(...) return ... end; 358 + }; 359 + toHTML = { 360 + elt = elt; 361 + tag = function(t,attrs,body) 362 + return f('%s%s</%s>', elt(t,attrs), body, t) 363 + end; 364 + catenate = table.concat; 365 + }; 366 + } 367 + end 368 + 369 + local function getBaseRenderers(procs, span_renderers) 370 + local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 371 + local htmlDoc = function(title, head, body) 372 + return [[<!doctype html>]] .. tag('html',nil, 373 + tag('head', nil, 374 + elt('meta',{charset = 'utf-8'}) .. 375 + (title and tag('title', nil, title) or '') .. 376 + (head or '')) .. 377 + tag('body', nil, body or '')) 378 + end 379 + 380 + local function htmlSpan(spans, block, sec) 381 + local text = {} 382 + for k,v in pairs(spans) do 383 + if type(v) == 'string' then 384 + v=v:gsub('[<>&"]', function(x) 385 + return string.format('&#%02u;', string.byte(x)) 386 + end) 387 + for fn, ext in renderJob:each('hook','render_html_sanitize') do 388 + v = fn(renderJob:delegate(ext), v) 389 + end 390 + table.insert(text,v) 391 + else 392 + table.insert(text, (span_renderers[v.kind](v, block, sec))) 393 + end 394 + end 395 + return table.concat(text) 396 + end 397 + return {htmlDoc=htmlDoc, htmlSpan=htmlSpan} 398 + end 399 + 400 + local spanparse = function(...) 401 + local s = ct.parse_span(...) 402 + doc.docjob:hook('meddle_span', s) 403 + return s 404 + end 405 + 406 + local cssRulesFor = {} 407 + local function getSpanRenderers(procs) 408 + local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 409 + local span_renderers = {} 410 + local plainrdr = getBaseRenderers(tagproc.toTXT, span_renderers) 411 + local htmlSpan = getBaseRenderers(procs, span_renderers).htmlSpan 412 + 413 + function span_renderers.format(sp,...) 414 + local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'} 415 + if sp.style == 'literal' and not opts['fossil-uv'] then 416 + stylesNeeded.code = true 417 + elseif sp.style == 'strike' or sp.style == 'insert' then 418 + stylesNeeded.editors_markup = true 419 + elseif sp.style == 'variable' then 420 + stylesNeeded.var = true 421 + end 422 + return tag(tags[sp.style],nil,htmlSpan(sp.spans,...)) 423 + end 424 + 425 + function span_renderers.deref(t,b,s) 426 + local r = b.origin:ref(t.ref) 427 + local name = t.ref 428 + if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end 429 + if type(r) == 'string' then 430 + stylesNeeded.abbr = true 431 + return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name) 432 + end 433 + if r.kind == 'resource' then 434 + local rid = getSafeID(r, 'res-') 435 + if r.class == 'image' then 436 + if not cssRulesFor[r] then 437 + local css = prepcss(string.format([[ 438 + section p > .%s { 439 + } 440 + ]], rid)) 441 + stylesets[r] = css 442 + cssRulesFor[r] = css 443 + stylesNeeded[r] = true 444 + end 445 + return tag('div',{class=rid},catenate{'blaah'}) 446 + elseif r.class == 'video' then 447 + local vid = {} 448 + return tag('video',nil,vid) 449 + elseif r.class == 'font' then 450 + b.origin:fail('fonts cannot be instantiated, use %font directive instead') 451 + end 452 + else 453 + b.origin:fail('%s is not an object that can be embedded', t.ref) 454 + end 455 + end 456 + 457 + function span_renderers.var(v,b,s) 458 + local val 459 + if v.pos then 460 + if not v.origin.invocation then 461 + v.origin:fail 'positional arguments can only be used in a macro invocation' 462 + elseif not v.origin.invocation.args[v.pos] then 463 + v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) 464 + end 465 + val = v.origin.invocation.args[v.pos] 466 + else 467 + val = v.origin.doc:context_var(v.var, v.origin) 468 + end 469 + if v.raw then 470 + return val 471 + else 472 + return htmlSpan(ct.parse_span(val, v.origin), b, s) 473 + end 474 + end 475 + 476 + function span_renderers.raw(v,b,s) 477 + return htmlSpan(v.spans, b, s) 478 + end 479 + 480 + function span_renderers.link(sp,b,s) 481 + local href 482 + if b.origin.doc.sections[sp.ref] then 483 + href = '#' .. sp.ref 484 + else 485 + if sp.addr then href = sp.addr else 486 + local r = b.origin:ref(sp.ref) 487 + if type(r) == 'table' then 488 + href = '#' .. getSafeID(r) 489 + else href = r end 490 + end 491 + end 492 + return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href) 493 + end 494 + 495 + span_renderers['line-break'] = function(sp,b,s) 496 + return elt('br') 497 + end 498 + 499 + function span_renderers.macro(m,b,s) 500 + local macroname = plainrdr.htmlSpan( 501 + ct.parse_span(m.macro, b.origin), b,s) 502 + local r = b.origin:ref(macroname) 503 + if type(r) ~= 'string' then 504 + b.origin:fail('%s is an object, not a reference', t.ref) 505 + end 506 + local mctx = b.origin:clone() 507 + mctx.invocation = m 508 + return htmlSpan(ct.parse_span(r, mctx),b,s) 509 + end 510 + function span_renderers.math(m,b,s) 511 + stylesNeeded.math = true 512 + local spans = {} 513 + local function fmt(sp, target) 514 + for i,v in ipairs(sp) do 515 + if type(v) == 'string' then 516 + local x = ct.tool.mathfmt(b.origin, v) 517 + for _,v in ipairs(x) do 518 + table.insert(target, v) 519 + end 520 + elseif type(v) == 'table' then 521 + if v.spans then 522 + local tbl = ss.delegate(v) 523 + tbl.spans = {} 524 + fmt(v.spans, tbl.spans) 525 + table.insert(target, tbl) 526 + else 527 + table.insert(target, v) 528 + end 529 + end 530 + end 531 + end 532 + fmt(m.spans,spans) 533 + 534 + return tag('span',{class='equation'},htmlSpan(spans, b, s)) 535 + end; 536 + function span_renderers.directive(d,b,s) 537 + if d.ext == 'html' then 538 + elseif b.origin.doc:allow_ext(d.ext) then 539 + elseif d.crit then 540 + b.origin:fail('critical extension %s unavailable', d.ext) 541 + elseif d.failthru then 542 + return htmlSpan(d.spans, b, s) 543 + end 544 + end 545 + function span_renderers.footnote(f,b,s) 546 + stylesNeeded.footnote = true 547 + local source, sid, ssec = b.origin:ref(f.ref) 548 + local cnc = getSafeID(ssec) .. ' ' .. sid 549 + local fn 550 + if footnotes[cnc] then 551 + fn = footnotes[cnc] 552 + else 553 + footnotecount = footnotecount + 1 554 + fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source} 555 + fn.id = getSafeID(fn) 556 + footnotes[cnc] = fn 557 + end 558 + return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) .. 559 + tag('sup',nil, fn.num)) 560 + end 561 + 562 + return span_renderers 563 + end 564 + 565 + local function getBlockRenderers(procs, sr) 566 + local tag, elt, catenate = procs.tag, procs.elt, procs.catenate 567 + local null = function() return catenate{} end 568 + 569 + local block_renderers = { 570 + anchor = function(b,s) 571 + return tag('a',{id = getSafeID(b)},null()) 572 + end; 573 + paragraph = function(b,s) 574 + stylesNeeded.paragraph = true; 575 + return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) 576 + end; 577 + directive = function(b,s) 578 + -- deal with renderer directives 579 + local _, cmd, args = b.words(2) 580 + if cmd == 'page-title' then 581 + if not opts.title then doctitle = args end 582 + elseif b.critical then 583 + b.origin:fail('critical HTML renderer directive “%s” not supported', cmd) 584 + end 585 + end; 586 + label = function(b,s) 587 + if ct.sec.is(b.captions) then 588 + if not (opts['fossil-uv'] or opts.snippet) then 589 + stylesNeeded.header = true 590 + end 591 + local h = math.min(6,math.max(1,b.captions.depth)) 592 + return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b) 593 + else 594 + -- handle other uses of labels here 595 + end 596 + end; 597 + ['list-item'] = function(b,s) 598 + return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) 599 + end; 600 + table = function(b,s) 601 + local tb = {} 602 + for i, r in ipairs(b.rows) do 603 + local row = {} 604 + for i, c in ipairs(r) do 605 + table.insert(row, tag(c.header and 'th' or 'td', 606 + {align=c.align}, sr.htmlSpan(c.spans, b))) 607 + end 608 + table.insert(tb, tag('tr',nil,catenate(row))) 609 + end 610 + return tag('table',nil,catenate(tb)) 611 + end; 612 + listing = function(b,s) 613 + stylesNeeded.block_code_listing = true 614 + local nodes = ss.map(function(l) 615 + if #l > 0 then 616 + return tag('div',nil,sr.htmlSpan(l, b, s)) 617 + else 618 + return elt('hr') 619 + end 620 + end, b.lines) 621 + if b.title then 622 + table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title))) 623 + end 624 + if b.lang then langsused[b.lang] = true end 625 + return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) 626 + end; 627 + aside = function(b,s) 628 + local bn = {} 629 + stylesNeeded.aside = true 630 + if #b.lines == 1 then 631 + bn[1] = sr.htmlSpan(b.lines[1], b, s) 632 + else 633 + for _,v in pairs(b.lines) do 634 + table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s))) 635 + end 636 + end 637 + return tag('aside', {}, bn) 638 + end; 639 + ['break'] = function() -- HACK 640 + -- lists need to be rewritten to work like asides 641 + return ''; 642 + end; 643 + } 644 + return block_renderers; 645 + end 646 + 647 + local function getRenderers(procs) 648 + local span_renderers = getSpanRenderers(procs) 649 + local r = getBaseRenderers(procs,span_renderers) 650 + r.block_renderers = getBlockRenderers(procs, r) 651 + return r 652 + end 653 + 654 + local astproc = { 655 + toHTML = getRenderers(tagproc.toHTML); 656 + toTXT = getRenderers(tagproc.toTXT); 657 + toIR = { }; 658 + } 659 + astproc.toIR.span_renderers = ss.clone(astproc.toHTML); 660 + astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML); 661 + -- note we use HTML here instead of IR span renderers, because as things 662 + -- currently stand we don't need that level of resolution. if we ever 663 + -- get to the point where we want to be able to twiddle spans around 664 + -- we'll need to introduce an IR span renderer 665 + 666 + render_state_handle.astproc = astproc; 667 + render_state_handle.tagproc = tagproc; 668 + 669 + -- bind to legacy names 670 + -- yikes this needs to be cleaned up so badly 671 + local ir = {} 672 + local dr = astproc.toHTML -- default renderers 673 + local plainr = astproc.toTXT 674 + local irBlockRdrs = astproc.toIR.block_renderers; 675 + 676 + render_state_handle.ir = ir; 677 + 678 + local function renderBlocks(blocks, irs) 679 + for i, block in ipairs(blocks) do 680 + local rd 681 + if irBlockRdrs[block.kind] then 682 + rd = irBlockRdrs[block.kind](block,sec) 683 + else 684 + local rdr = renderJob:proc('render',block.kind,'html') 685 + if rdr then 686 + rd = rdr({ 687 + state = render_state_handle; 688 + tagproc = tagproc.toIR; 689 + astproc = astproc.toIR; 690 + }, block, sec) 691 + end 692 + end 693 + if rd then 694 + if opts['heading-anchors'] and block == sec.heading_node then 695 + stylesNeeded.headingAnchors = true 696 + table.insert(rd.nodes, ' ') 697 + table.insert(rd.nodes, { 698 + tag = 'a'; 699 + attrs = {href = '#' .. irs.attrs.id, class='anchor'}; 700 + nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'}; 701 + }) 702 + end 703 + if rd.src and rd.src.origin.lang then 704 + if not rd.attrs then rd.attrs = {} end 705 + rd.attrs.lang = rd.src.origin.lang 706 + end 707 + table.insert(irs.nodes, rd) 708 + runhook('ir_section_node_insert', rd, irs, sec) 709 + end 710 + end 711 + end 712 + runhook('ir_assemble', ir) 713 + for i, sec in ipairs(doc.secorder) do 714 + if doctitle == nil and sec.depth == 1 and sec.heading_node then 715 + doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec) 716 + end 717 + local irs 718 + if sec.kind == 'ordinary' then 719 + if #(sec.blocks) > 0 then 720 + irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}} 721 + runhook('ir_section_build', irs, sec) 722 + renderBlocks(sec.blocks, irs) 723 + end 724 + elseif sec.kind == 'blockquote' then 725 + elseif sec.kind == 'listing' then 726 + elseif sec.kind == 'embed' then 727 + end 728 + if irs then table.insert(ir, irs) end 729 + end 730 + 731 + for _, fn in pairs(footnotes) do 732 + local tag = tagproc.toIR.tag 733 + local body = {nodes={}} 734 + local ftir = {} 735 + for l in fn.source:gmatch('([^\n]*)') do 736 + ct.parse_line(l, fn.origin, ftir) 737 + end 738 + renderBlocks(ftir,body) 739 + local note = tag('div',{class='footnote',id=fn.id}, { 740 + tag('div',{class='number'}, tostring(fn.num)), 741 + tag('div',{class='text'}, body.nodes), 742 + tag('a',{href='#0'},'close') 743 + }) 744 + table.insert(ir, note) 745 + end 746 + 747 + -- restructure passes 748 + runhook('ir_restructure_pre', ir) 749 + 750 + ---- list insertion pass 751 + local lists = {} 752 + for _, sec in pairs(ir) do 753 + if sec.tag == 'section' then 754 + local i = 1 while i <= #sec.nodes do local v = sec.nodes[i] 755 + if v.tag == 'li' then 756 + local ltag 757 + if v.src.ordered 758 + then ltag = 'ol' 759 + else ltag = 'ul' 760 + end 761 + local last = i>1 and sec.nodes[i-1] 762 + if last and last.embed == 'list' and not ( 763 + last.ref[#last.ref].src.depth == v.src.depth and 764 + last.ref[#last.ref].src.ordered ~= v.src.ordered 765 + ) then 766 + -- add to existing list 767 + table.insert(last.ref, v) 768 + table.remove(sec.nodes, i) i = i - 1 769 + else 770 + -- wrap in list 771 + local newls = {v} 772 + sec.nodes[i] = {embed = 'list', ref = newls} 773 + table.insert(lists,newls) 774 + end 775 + end 776 + i = i + 1 end 777 + end 778 + end 779 + 780 + for _, sec in pairs(ir) do 781 + if sec.tag == 'section' then 782 + for i, elt in pairs(sec.nodes) do 783 + if elt.embed == 'list' then 784 + local function fail_nest() 785 + elt.ref[1].src.origin:fail('improper list nesting') 786 + end 787 + local struc = {attrs={}, nodes={}} 788 + if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end 789 + if elt.ref[1].src.depth ~= 1 then fail_nest() end 790 + 791 + local stack = {struc} 792 + local copyNodes = function(old,new) 793 + for i,v in ipairs(old) do new[#new + i] = v end 794 + end 795 + for i,e in ipairs(elt.ref) do 796 + if e.src.depth > #stack then 797 + if e.src.depth - #stack > 1 then fail_nest() end 798 + local newls = {attrs={}, nodes={e}} 799 + copyNodes(e.nodes,newls) 800 + if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end 801 + table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls) 802 + table.insert(stack, newls) 803 + else 804 + if e.src.depth < #stack then 805 + -- pop entries off the stack 806 + for i=#stack, e.src.depth+1, -1 do stack[i] = nil end 807 + end 808 + table.insert(stack[#stack].nodes, e) 809 + end 810 + end 811 + 812 + sec.nodes[i] = struc 813 + end 814 + end 815 + end 816 + end 817 + 818 + runhook('ir_restructure_post', ir) 819 + 820 + -- collection pass 821 + local function collect_nodes(t) 822 + local ts = '' 823 + for i,v in ipairs(t) do 824 + if type(v) == 'string' then 825 + ts = ts .. v 826 + elseif v.nodes then 827 + ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes)) 828 + elseif v.text then 829 + ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text) 830 + else 831 + ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs) 832 + end 833 + end 834 + return ts 835 + end 836 + local body = collect_nodes(ir) 837 + 838 + for k in pairs(langsused) do 839 + local spec = langpairs[k] or {color=0xaaaaaa} 840 + stylesets.block_code_listing = stylesets.block_code_listing .. string.format( 841 + [[section > figure.listing[lang="%s"]>figcaption::after 842 + { content: '%s'; color: #%06x }]], 843 + k, spec.name or k, spec.color) 844 + end 845 + 846 + local prepcss = function(css) 847 + local tone = function(fac, sat, sep, alpha) 848 + local hsl = function(h,s,l,a) 849 + local v = string.format('%s, %u%%, %u%%', h,s,l) 850 + if a then 851 + return string.format('hsla(%s, %s)', v,a) 852 + else 853 + return string.format('hsl(%s)', v) 854 + end 855 + end 856 + sat = sat or 1 857 + fac = math.max(math.min(fac, 1), 0) 858 + sat = math.max(math.min(sat, 1), 0) 859 + if opts.accent then 860 + local hue = 'var(--accent)' 861 + local hsep = tonumber(opts['hue-spread']) 862 + if hsep and sep and sep ~= 0 then 863 + hue = string.format('calc(%s - %s)', hue, sep * hsep) 864 + end 865 + return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha) 866 + else 867 + local g = math.floor(0xFF * fac) 868 + return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha)) 869 + end 870 + end 871 + local replace = function(var,alpha,param) 872 + local tonespan = opts.accent and .1 or 0 873 + local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan 874 + local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan 875 + if var == 'bg' then 876 + return tone(tbg,nil,nil,tonumber(alpha)) 877 + elseif var == 'fg' then 878 + return tone(tfg,nil,nil,tonumber(alpha)) 879 + elseif var == 'width' then 880 + return opts['width'] or '100vw' 881 + elseif var == 'tone' then 882 + local l, sep, sat 883 + for i=1,3 do -- 🙄 884 + l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$') 885 + if l then break end 886 + end 887 + l = ss.math.lerp(tonumber(l), tbg, tfg) 888 + return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha)) 889 + end 890 + end 891 + css = css:gsub('@(%b[]):(%b[])', function(v,d) return opts[v:sub(2,-2)] or v:sub(2,-2) end) 892 + css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace) 893 + css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end) 894 + css = css:gsub('@(%w+)/([0-9.]+)', replace) 895 + css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) 896 + return (css:gsub('%s+',' ')) 897 + end 898 + 899 + local styles = {} 900 + if opts.width then 901 + table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width)) 902 + end 903 + if opts.accent then 904 + table.insert(styles, string.format(':root {--accent:%s}', opts.accent)) 905 + end 906 + if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then 907 + stylesNeeded.accent = true 908 + end 909 + 910 + 911 + for k in pairs(stylesNeeded) do 912 + if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end 913 + table.insert(styles, prepcss(stylesets[k])) 914 + end 915 + 916 + local head = {} 917 + local styletag = '' 918 + if opts['link-css'] then 919 + local css = opts['link-css'] 920 + if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end 921 + styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']}) 922 + end 923 + if next(styles) then 924 + if opts['gen-styles'] then 925 + styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles)) 926 + end 927 + table.insert(head, styletag) 928 + end 929 + 930 + if opts['fossil-uv'] then 931 + return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body) 932 + elseif opts.snippet then 933 + return styletag .. body 934 + else 935 + return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) 936 + end 937 +end
Modified sirsem.lua from [581e1b0127] to [dc1f0ae1fb].
212 212 ascii = { 213 213 len = string.len; char = string.char; codepoint = string.byte; 214 214 iswhitespace = function(c) 215 215 return (c == ' ') or (c == '\t') or (c == '\n') 216 216 end; 217 217 ranges = { 218 218 {0x00,0x1a, cc.ctl}; 219 - {0x1b,0x1b, cc.ctl, cp.disallow}; 219 + {0x1b,0x1b, cc.ctl | cp.disallow}; 220 220 {0x1c,0x1f, cc.ctl}; 221 221 {0x20,0x20, cc.space}; 222 222 {0x21,0x22, cc.punct}; 223 223 {0x23,0x26, cc.symbol}; 224 224 {0x27,0x29, cc.punct}; 225 225 {0x2a,0x2b, cc.symbol}; 226 226 {0x2c,0x2f, cc.punct}; 227 - {0x30,0x39, cc.numeral, cp.hexnumeral}; 227 + {0x30,0x39, cc.numeral | cp.hexnumeral}; 228 228 {0x3a,0x3b, cc.punct}; 229 - {0x3c,0x3e, cc.symbol, cp.mathop}; 229 + {0x3c,0x3e, cc.symbol | cp.mathop}; 230 230 {0x3f,0x3f, cc.punct}; 231 231 {0x40,0x40, cc.symbol}; 232 - {0x41,0x46, cc.letter, cp.ucase, cp.hexnumeral}; 233 - {0x47,0x5a, cc.letter, cp.ucase}; 234 - {0x5b,0x5d, cc.symbol, cp.mathop}; 235 - {0x5e,0x5e, cc.symbol, mathop}; 232 + {0x41,0x46, cc.letter | cp.upper | cp.hexnumeral}; 233 + {0x47,0x5a, cc.letter | cp.upper}; 234 + {0x5b,0x5d, cc.symbol | cp.mathop}; 235 + {0x5e,0x5e, cc.symbol | cp.mathop}; 236 236 {0x5f,0x60, cc.symbol}; 237 - {0x61,0x66, cc.letter, cp.lcase, cp.hexnumeral}; 238 - {0x67,0x7a, cc.letter, cp.lcase}; 237 + {0x61,0x66, cc.letter | cp.lower | cp.hexnumeral}; 238 + {0x67,0x7a, cc.letter | cp.lower}; 239 239 {0x7b,0x7e, cc.symbol}; 240 240 {0x7f,0x7f, cc.ctl, cp.disallow}; 241 241 } 242 242 }; 243 243 raw = {len = string.len; char = string.char; codepoint = string.byte; 244 244 encodeUCS = function(str) return str end; 245 245 iswhitespace = function(c) ................................................................................ 250 250 251 251 -- unicode ranges are optionally generated from consortium data 252 252 -- files and injected through a generated source file. if this 253 253 -- part of the build process is disabled (e.g. due to lack of 254 254 -- internet access, or to keep the size of the executable as 255 255 -- small as possible), we still at least can make the ascii 256 256 -- ranges available to UTF8 (UTF8 being a superset of ascii) 257 -ss.str.enc.utf8.ranges = ss.delegate(ss.str.enc.ascii.ranges) 257 +ss.str.enc.utf8.ranges = ss.str.enc.ascii.ranges 258 258 259 259 function ss.str.enc.ascii.encodeUCS(str) 260 260 local newstr = '' 261 261 for c,p in ss.str.each(ss.str.enc.utf8, str, true) do 262 262 if c > 0x7F then 263 263 newstr = newstr .. '?' 264 264 else ................................................................................ 266 266 end 267 267 end 268 268 end 269 269 270 270 for _, v in pairs{'utf8','ascii','raw'} do 271 271 ss.str.enc[v].parse_escape = ss.str.enc_generics.pfxescape('\\',ss.str.enc[v]) 272 272 end 273 + 274 +function ss.bitmask_expand(ty, v) 275 + local bitrange = ty[true] 276 + local fb 277 + if bitrange[1] ~= 0 then 278 + fb = v & ((1<<bitrange[1]) - 1) -- first N bits 279 + end 280 + local tbl = {} 281 + for j=bitrange[1], bitrange[2] do 282 + if (fb & (1<<j)) ~= 0 then 283 + tbl[ty[1<<j]] = true 284 + end 285 + end 286 + return tbl, fb 287 +end 273 288 274 289 function ss.str.classify(enc, ch) 275 290 if not enc.ranges then return {} end 276 291 if type(ch)=='string' then ch = enc.codepoint(ch) end 277 - -- TODO 292 + 293 + for _, r in pairs(enc.ranges) do 294 + if ch >= r[1] and ch <= r[2] then 295 + local p,b = ss.bitmask_expand(ss.str.charprop, r[3]) 296 + if b then p[ss.str.charclass[b]] = true end 297 + return p 298 + end 299 + end 300 + 301 + return {} 278 302 end 279 303 280 304 281 305 function ss.str.each(enc, str, ascode) 282 306 if enc.each then return enc.each(enc,str,ascode) end 283 307 local pm = { 284 308 __index = {
Modified tools/ucs.lua from [3976f4bc78] to [cf6aee3c65].
21 21 local file = io.stdin 22 22 local path 23 23 if arg[1] then 24 24 path = arg[1] 25 25 file = io.open(path, 'rb') 26 26 end 27 27 28 -local bitmask_raw = function(n,ofs) 29 - ofs = ofs or 0 30 - local function rec(i) 31 - if i > n then return end 32 - return 1<<(i+ofs), rec(i+1) 33 - end 34 - return 1<<ofs, rec(1) 35 -end 36 - 37 -local bitmask = function(tbl,ofs) 38 - local codes = {bitmask_raw(#tbl,ofs)} 39 - local m = {} 40 - local maxbit 41 - for i, s in ipairs(tbl) do 42 - m[s] = codes[i] 43 - m[codes[i]] = s 44 - maxbit = i 45 - end 46 - m[true] = {ofs or 0,maxbit} 47 - return m 48 -end 49 - 50 -local basictype = enum { 51 - 'numeral'; 52 - 'alpha'; 53 - 'symbol'; 54 - 'punct'; 55 - 'space'; 56 - 'ctl'; 57 - 'glyph'; -- hanji 58 -} 59 -local props = bitmask({ 60 - 'hex', 61 - 'upper', 'lower', 'diac', 62 - 'wordbreak', 'wordsep', 63 - 'disallow', 64 - 'brack', 'right', 'left', 65 - 'noprint', 'superimpose' 66 -}, 3) 67 - 28 +local ss = require'sirsem' 29 +local basictype = ss.str.charclass 30 +local props = ss.str.charprop 68 31 local overrides = { 69 32 [0x200B] = basictype.space | props.wordsep; -- database entry is wrong 70 33 } 71 34 72 35 local mask = ~0 -- mask out irrelevant properties to compactify database 73 36 74 37 local function parsecat(tbl) ................................................................................ 78 41 elseif tbl.class == 'Nd' then c = b.numeral 79 42 elseif tbl.class == 'No' then c = b.numeral | p.diac 80 43 elseif tbl.class == 'Cc' then 81 44 if tbl.kind == 'S' 82 45 or tbl.kind == 'WS' 83 46 or tbl.kind == 'B' then c = b.space | p.wordsep 84 47 else c = b.ctl | p.wordbreak | p.disallow end 85 - elseif tbl.class == 'Lu' then c = b.alpha | p.upper 86 - elseif tbl.class == 'Ll' then c = b.alpha | p.lower 48 + elseif tbl.class == 'Lu' then c = b.letter | p.upper 49 + elseif tbl.class == 'Ll' then c = b.letter | p.lower 87 50 elseif tbl.class == 'Lo' 88 - or tbl.class == 'Lt' then c = b.alpha 51 + or tbl.class == 'Lt' then c = b.letter 89 52 elseif tbl.class == 'Po' then c = b.punct | p.wordbreak 90 53 elseif tbl.class == 'Sm' then c = b.symbol | p.wordsep 91 54 elseif tbl.class == 'Ps' then c = b.punct | p.brack | p.left 92 55 elseif tbl.class == 'Pe' then c = b.punct | p.brack | p.right 93 56 elseif tbl.class == 'Pc' 94 57 or tbl.class == 'Pd' 95 58 or tbl.class == 'Sk' ................................................................................ 104 67 105 68 local ranuirAlpha = {0xe39d, 0xe39f, 0xe3ad, 0xe3af, 0xe3b5, 0xe3b7, 0xe3b9, 0xe3bb, 0xe3bd, 0xe3be, 0xe3bf, 0xe3c5, 0xe3c7, 0xe3c9, 0xe3cb, 0xe3cc, 0xe3cd, 0xe3ce, 0xe3cf} 106 69 local ranuirSpecial = { 107 70 [0xe390] = basictype.space | props.wordsep; 108 71 } 109 72 110 73 local ranuir = {} 111 -for _,v in pairs(ranuirAlpha) do ranuir[v] = basictype.alpha end 74 +for _,v in pairs(ranuirAlpha) do ranuir[v] = basictype.letter end 112 75 for k,v in pairs(ranuirSpecial) do ranuir[k] = v end 113 76 local ranuirKeys = {} 114 77 for k in pairs(ranuir) do table.insert(ranuirKeys, k) end 115 78 table.sort(ranuirKeys) 116 79 117 80 local recs = {} 118 81 local ranuirok = false 119 82 for ln in file:lines() do 120 83 local v = {} 121 84 for s in ln:gmatch('[^;]*') do 122 85 table.insert(v, s) 123 86 end 124 87 v[1] = tonumber(v[1],0x10) 125 - if v[1] > 0x7f then -- discard ASCII, we already have that 88 +-- if v[1] > 0x7f then -- discard ASCII, we already have that 126 89 local code = { 127 90 codepoint = v[1]; 128 91 name = v[2]; 129 92 class = v[3]; 130 93 kind = v[5]; 131 94 } 132 95 code.cat = parsecat(code) ................................................................................ 140 103 end 141 104 ranuirok = true 142 105 end 143 106 144 107 if code.cat ~= 0 then 145 108 table.insert(recs,code) 146 109 end 147 - end 110 +-- end 148 111 end 149 112 150 113 151 114 local ranges = {} 152 115 local last = recs[1] 153 116 local start = last 154 117 local altern = false ................................................................................ 179 142 flush() 180 143 start = r 181 144 end 182 145 last = r 183 146 end 184 147 flush() 185 148 186 --- expand bitmask 187 - -- for k,v in pairs(ranges) do 188 - -- local basic = v[3] & ((1<<3) - 1) -- first three bits 189 - -- if basic ~= 0 then 190 - -- v[4] = basictype[basic] 191 - -- end 192 - -- local bitrange = props[true] 193 - -- for j=bitrange[1], bitrange[2] do 194 - -- if (v[3] & (1<<j)) ~= 0 then 195 - -- table.insert(v, props[1<<j]) 196 - -- end 197 - -- end 198 - -- end 199 - 200 149 -- the data has been collected and formatted in the manner we 201 150 -- need; now we just need to emit it as a lua table 202 151 203 152 local tab = {} 204 153 local top = 1 205 154 for k,v in pairs(ranges) do 206 155 tab[top] = string.format('{0x%x,0x%x,%u}',table.unpack(v)) 207 156 top = top + 1 208 157 end 209 -io.stdout:write(string.format(tpl, table.concat(tab,','))) 158 +io.stdout:write(string.format(tpl, table.concat(tab,',\n')))