hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 947954817949adb49df628cfa53ae6b3c4eb1dbe
parent 3f5cc750d181a1e173eec3046a7702a345413416
Author: Drew DeVault <sir@cmpwn.com>
Date:   Mon, 19 Apr 2021 14:34:12 -0400

haredoc: basic HTML formatter

Diffstat:
MMakefile | 5+++--
Mcmd/haredoc/hare.ha | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Acmd/haredoc/html.ha | 338+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/haredoc/main.ha | 16++++++++++++----
Acmd/haredoc/sort.ha | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcmd/haredoc/unparse.ha | 90-------------------------------------------------------------------------------
Mformat/html/escape.ha | 8++++----
7 files changed, 552 insertions(+), 115 deletions(-)

diff --git a/Makefile b/Makefile @@ -35,8 +35,9 @@ harec_srcs=\ haredoc_srcs=\ ./cmd/haredoc/main.ha \ ./cmd/haredoc/errors.ha \ - ./cmd/haredoc/unparse.ha \ - ./cmd/haredoc/hare.ha + ./cmd/haredoc/hare.ha \ + ./cmd/haredoc/html.ha \ + ./cmd/haredoc/sort.ha $(HARECACHE)/hare.ssa: $(hare_srcs) $(hare_stdlib_deps) @printf 'HAREC\t$@\n' diff --git a/cmd/haredoc/hare.ha b/cmd/haredoc/hare.ha @@ -1,26 +1,120 @@ use fmt; use hare::ast; +use hare::lex; +use hare::unparse; +use io; use os; use strings; +use strio; // Formats output as Hare source code (prototypes) -fn emit_hare(decls: []ast::decl) (void | error) = { +fn emit_hare(summary: summary) (void | error) = { // TODO: Should we emit the dependencies, too? - for (let i = 0z; i < len(decls); i += 1) { - const decl = decls[i]; - if (!decl.exported || decl.docs == "") { - continue; - }; + for (let i = 0z; i < len(summary.types); i += 1) { + details_hare(summary.types[i])?; + }; + for (let i = 0z; i < len(summary.globals); i += 1) { + details_hare(summary.globals[i])?; + }; + for (let i = 0z; i < len(summary.funcs); i += 1) { + details_hare(summary.funcs[i])?; + }; +}; - const iter = strings::tokenize(decl.docs, "\n"); - for (true) match (strings::next_token(&iter)) { - s: str => if (len(s) != 0) { - fmt::printfln("//{}", s)?; - }, - void => break, - }; +fn details_hare(decl: ast::decl) (void | error) = { + const iter = strings::tokenize(decl.docs, "\n"); + for (true) match (strings::next_token(&iter)) { + s: str => if (len(s) != 0) { + fmt::printfln("//{}", s)?; + }, + void => break, + }; - unparse_decl(os::stdout, decl)?; - fmt::print("\n\n")?; + unparse_hare(os::stdout, decl)?; + fmt::print("\n\n")?; + return; +}; + +// Forked from [[hare::unparse]] +fn unparse_hare(out: *io::stream, d: ast::decl) (size | io::error) = { + let n = 0z; + match (d.decl) { + g: []ast::decl_global => { + n += fmt::fprint(out, + if (g[0].is_const) "def " else "let ")?; + for (let i = 0z; i < len(g); i += 1) { + if (len(g[i].symbol) != 0) { + n += fmt::fprintf(out, + "@symbol(\"{}\") ", g[i].symbol)?; + }; + n += unparse::ident(out, g[i].ident)?; + n += fmt::fprint(out, ": ")?; + n += unparse::_type(out, 0, g[i]._type)?; + if (i + 1 < len(g)) { + n += fmt::fprint(out, ", ")?; + }; + }; + }, + t: []ast::decl_type => { + n += fmt::fprint(out, "type ")?; + for (let i = 0z; i < len(t); i += 1) { + n += unparse::ident(out, t[i].ident)?; + n += fmt::fprint(out, " = ")?; + n += unparse::_type(out, 0, t[i]._type)?; + if (i + 1 < len(t)) { + n += fmt::fprint(out, ", ")?; + }; + }; + }, + f: ast::decl_func => { + n += fmt::fprint(out, switch (f.attrs) { + ast::fndecl_attrs::NONE => "", + ast::fndecl_attrs::FINI => "@fini ", + ast::fndecl_attrs::INIT => "@init ", + ast::fndecl_attrs::TEST => "@test ", + })?; + let p = f.prototype._type as ast::func_type; + if (p.attrs & ast::func_attrs::NORETURN != 0) { + n += fmt::fprint(out, "@noreturn ")?; + }; + if (len(f.symbol) != 0) { + n += fmt::fprintf(out, "@symbol(\"{}\") ", + f.symbol)?; + }; + n += fmt::fprint(out, "fn ")?; + n += unparse::ident(out, f.ident)?; + n += prototype_hare(out, 0, + f.prototype._type as ast::func_type)?; + }, + }; + n += fmt::fprint(out, ";")?; + return n; +}; + +fn prototype_hare( + out: *io::stream, + indent: size, + t: ast::func_type, +) (size | io::error) = { + let n = 0z; + n += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t.params); i += 1) { + let param = t.params[i]; + n += fmt::fprintf(out, "{}: ", + if (len(param.name) == 0) "_" else param.name)?; + n += unparse::_type(out, indent, *param._type)?; + if (i + 1 == len(t.params) + && t.variadism == ast::variadism::HARE) { + n += fmt::fprintf(out, "...")?; + }; + if (i + 1 < len(t.params)) { + n += fmt::fprint(out, ", ")?; + }; + }; + if (t.variadism == ast::variadism::C) { + n += fmt::fprint(out, ", ...")?; }; + n += fmt::fprint(out, ") ")?; + n += unparse::_type(out, indent, *t.result)?; + return n; }; diff --git a/cmd/haredoc/html.ha b/cmd/haredoc/html.ha @@ -0,0 +1,338 @@ +use fmt; +use format::html; +use hare::ast; +use hare::lex; +use hare::unparse; +use io; +use os; +use strings; +use strio; + +// Formats output as HTML +fn emit_html(decls: summary, template: bool) (void | error) = { + if (template) head()?; + + // TODO: Module name + fmt::println("<h2>os</h2>")?; + fmt::println("<p class='readme'>TODO: Insert module README here")?; + if (len(decls.types) != 0) { + fmt::println("<h4>Types</h4>")?; + fmt::println("<ol>")?; + for (let i = 0z; i < len(decls.types); i += 1) { + tocentry(decls.types[i])?; + }; + fmt::println("</ol>")?; + }; + + if (len(decls.globals) != 0) { + fmt::println("<h4>Globals</h4>")?; + fmt::println("<ol>")?; + for (let i = 0z; i < len(decls.globals); i += 1) { + tocentry(decls.globals[i])?; + }; + fmt::println("</ol>")?; + }; + + if (len(decls.funcs) != 0) { + fmt::println("<h4>Functions</h4>")?; + fmt::println("<ol>")?; + for (let i = 0z; i < len(decls.funcs); i += 1) { + tocentry(decls.funcs[i])?; + }; + fmt::println("</ol>")?; + }; + fmt::println("</ol>")?; + + for (let i = 0z; i < len(decls.types); i += 1) { + details(decls.types[i])?; + }; + for (let i = 0z; i < len(decls.globals); i += 1) { + details(decls.globals[i])?; + }; + for (let i = 0z; i < len(decls.funcs); i += 1) { + details(decls.funcs[i])?; + }; +}; + +fn tocentry(decl: ast::decl) (void | error) = { + fmt::println("<li>"); + fmt::printf("{} ", + match (decl.decl) { + ast::decl_func => "fn", + []ast::decl_type => "type", + []ast::decl_global => "let", + }); + fmt::printf("<a href='#"); + unparse::ident(os::stdout, decl_ident(decl))?; + fmt::printf("'>"); + unparse::ident(os::stdout, decl_ident(decl))?; + fmt::print("</a>"); + + match (decl.decl) { + g: []ast::decl_global => { + let g = g[0]; + fmt::fprint(os::stdout, ": ")?; + type_html(os::stdout, 0, g._type, true)?; + }, + t: []ast::decl_type => { + let t = t[0]; + fmt::fprint(os::stdout, " = ")?; + type_html(os::stdout, 0, t._type, true)?; + }, + f: ast::decl_func => prototype_html(os::stdout, 0, + f.prototype._type as ast::func_type, + true)?, + }; + fmt::println(";"); + + fmt::println("</li>"); + return; +}; + +fn details(decl: ast::decl) (void | error) = { + fmt::println("<section class='member'>"); + fmt::print("<h3 id='")?; + unparse::ident(os::stdout, decl_ident(decl))?; + fmt::print("'>")?; + unparse::ident(os::stdout, decl_ident(decl))?; + fmt::println("</h3>")?; + + fmt::println("<pre class='decl'>")?; + unparse_html(os::stdout, decl)?; + fmt::println("</pre>")?; + + // TODO: Apply Hare formatting + fmt::println("<p class='comment'>")?; + const iter = strings::tokenize(decl.docs, "\n"); + for (true) match (strings::next_token(&iter)) { + s: str => if (len(s) != 0) { + html::escape(os::stdout, s)?; + }, + void => break, + }; + + fmt::println("</section>")?; + return; +}; + +// Forked from [[hare::unparse]] +fn unparse_html(out: *io::stream, d: ast::decl) (size | io::error) = { + // TODO: Syntax highlighting + let n = 0z; + match (d.decl) { + g: []ast::decl_global => { + n += fmt::fprintf(out, "<span class='keyword'>{}</span>", + if (g[0].is_const) "def " else "let ")?; + for (let i = 0z; i < len(g); i += 1) { + n += unparse::ident(out, g[i].ident)?; + n += fmt::fprint(out, ": ")?; + n += type_html(out, 0, g[i]._type, false)?; + if (i + 1 < len(g)) { + n += fmt::fprint(out, ", ")?; + }; + }; + }, + t: []ast::decl_type => { + n += fmt::fprint(out, "<span class='keyword'>type</span> ")?; + for (let i = 0z; i < len(t); i += 1) { + n += unparse::ident(out, t[i].ident)?; + n += fmt::fprint(out, " = ")?; + n += type_html(out, 0, t[i]._type, false)?; + if (i + 1 < len(t)) { + n += fmt::fprint(out, ", ")?; + }; + }; + }, + f: ast::decl_func => { + n += fmt::fprint(out, switch (f.attrs) { + ast::fndecl_attrs::NONE => "", + ast::fndecl_attrs::FINI => "@fini ", + ast::fndecl_attrs::INIT => "@init ", + ast::fndecl_attrs::TEST => "@test ", + })?; + let p = f.prototype._type as ast::func_type; + if (p.attrs & ast::func_attrs::NORETURN != 0) { + n += fmt::fprint(out, "@noreturn ")?; + }; + n += fmt::fprint(out, "<span class='keyword'>fn</span> ")?; + n += unparse::ident(out, f.ident)?; + n += prototype_html(out, 0, + f.prototype._type as ast::func_type, + false)?; + }, + }; + n += fmt::fprint(out, ";")?; + return n; +}; + +fn type_html( + out: *io::stream, + indent: size, + _type: ast::_type, + brief: bool, +) (size | io::error) = { + let buf = strio::dynamic(); + defer io::close(buf); + unparse::_type(buf, indent, _type)?; + if (brief) { + return html::escape(out, strio::string(buf))?; + }; + + // TODO: More detailed formatter which can find aliases nested deeper in + // other types + let z = 0z; + match (_type._type) { + ast::alias_type => { + // TODO: Make this link real + z += fmt::fprint(out, "<a href='#'>")?; + z += html::escape(out, strio::string(buf))?; + z += fmt::fprint(out, "</a>")?; + }, + ast::builtin_type => { + z += fmt::fprint(out, "<span class='type'>")?; + z += html::escape(out, strio::string(buf))?; + z += fmt::fprintf(out, "</span>")?; + }, + * => z += html::escape(out, strio::string(buf))?, + }; + z; +}; + +fn prototype_html( + out: *io::stream, + indent: size, + t: ast::func_type, + brief: bool, +) (size | io::error) = { + let n = 0z; + n += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t.params); i += 1) { + let param = t.params[i]; + if (!brief) { + n += fmt::fprintf(out, "{}: ", + if (len(param.name) == 0) "_" else param.name)?; + }; + n += type_html(out, indent, *param._type, brief)?; + if (i + 1 == len(t.params) + && t.variadism == ast::variadism::HARE) { + n += fmt::fprintf(out, "...")?; + }; + if (i + 1 < len(t.params)) { + n += fmt::fprint(out, ", ")?; + }; + }; + if (t.variadism == ast::variadism::C) { + n += fmt::fprint(out, ", ...")?; + }; + n += fmt::fprint(out, ") ")?; + n += type_html(out, indent, *t.result, brief)?; + return n; +}; + +fn head() (void | error) = { + // TODO: Get title from module name + // TODO: Move me to +embed? + fmt::println("<!doctype html> +<html lang='en'> +<meta charset='utf-8' /> +<title>Hare documentation</title> +<style> +body { + font-family: sans-serif; + line-height: 1.3; + margin: 0 auto; +} + +nav:not(#TableOfContents) { + max-width: 1000px; + margin: 1rem auto 0; + display: grid; + grid-template-rows: auto auto 1fr; + grid-template-columns: auto 1fr; + grid-template-areas: + 'logo header' + 'logo nav' + 'logo none'; +} + +nav:not(#TableOfContents) img { + grid-area: logo; +} + +nav:not(#TableOfContents) h1 { + grid-area: header; + margin: 0; + padding: 0; +} + +nav:not(#TableOfContents) ul { + grid-area: nav; + margin: 0.5rem 0 0 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; +} + +#TableOfContents { + font-size: 1.1rem; +} + +main { + padding: 0 128px; + max-width: 800px; + margin: 0 auto; + +} + +pre { + background-color: #eee; + padding: 0.25rem 1rem; + margin: 0 -1rem 1rem; +} + +pre .keyword { + color: #008; +} + +pre .type { + color: #44F; +} + +ol { + padding-left: 0; + list-style: none; +} + +ol li { + padding-left: 0; +} + +@media(max-width: 1000px) { + main { + padding: 0; + } +} +</style> +<nav> + <img + src='https://harelang.org/mascot.jpg' + alt='An inked drawing of the Hare mascot, a fuzzy rabbit' + width='128' height='128' /> + <h1>The Hare programming language</h1> + <ul> + <li><a href='https://harelang.org/'>Home</a></li> + <li><a href='https://harelang.org/documentation'>Documentation</a></li> + <li><a href='https://harelang.org/tutorial'>Tutorials</a></li> + <li><a href='https://harelang.org/blog'>Blog</a></li> + <li><a href='https://harelang.org/community'>Community</a></li> + <li><a href='https://sr.ht/~sircmpwn/hare'>Source code</a></li> + <li><a href='https://harelang.org/specification'>Specification</a></li> + </ul> +</nav> +<main> +")?; + return; +}; diff --git a/cmd/haredoc/main.ha b/cmd/haredoc/main.ha @@ -18,9 +18,11 @@ type format = enum { export fn main() void = { // TODO: Use format::TTY by default if stdout is a tty let fmt = format::HARE; + let template = true; const cmd = getopt::parse(os::args, "reads and formats Hare documentation", ('F', "format", "specify output format (hare, tty, html, or gemtext)"), + ('t', "disable HTML template (requries postprocessing)"), "[identifiers...]", ); defer getopt::finish(&cmd); @@ -34,6 +36,7 @@ export fn main() void = { else if (opt.1 == "html") format::HTML else if (opt.1 == "gemtext") format::GEMTEXT else fmt::fatal("Invalid format {}", opt.1), + 't' => template = false, * => abort(), }; }; @@ -54,7 +57,8 @@ export fn main() void = { }; for (let i = 0z; i < len(unit); i += 1) { - match (emit(unit[i].decls, fmt)) { + let summary = sort_decls(unit[i].decls); + match (emit(summary, fmt, template)) { void => void, err: error => fmt::fatal("Error: {}", strerror(err)), }; @@ -72,9 +76,13 @@ fn scan(path: str) (ast::subunit | error) = { return parse::subunit(&lexer)?; }; -fn emit(decls: []ast::decl, fmt: format) (void | error) = switch (fmt) { - format::HARE => emit_hare(decls), +fn emit( + summary: summary, + fmt: format, + template: bool, +) (void | error) = switch (fmt) { + format::HARE => emit_hare(summary), format::TTY => abort(), // TODO - format::HTML => abort(), // TODO + format::HTML => emit_html(summary, template)?, format::GEMTEXT => abort(), // TODO }; diff --git a/cmd/haredoc/sort.ha b/cmd/haredoc/sort.ha @@ -0,0 +1,86 @@ +use ascii; +use hare::ast; +use sort; + +type summary = struct { + // TODO: Constants + types: []ast::decl, + globals: []ast::decl, + funcs: []ast::decl, +}; + +// Sorts declarations by removing unexported declarations, moving undocumented +// declarations to the end, sorting by identifier, and ensuring that only one +// member is present in each declaration (so that "let x: int = 10, y: int = 20" +// becomes two declarations: "let x: int = 10; let y: int = 20;"). +fn sort_decls(decls: []ast::decl) summary = { + let sorted = summary { ... }; + + for (let i = 0z; i < len(decls); i += 1) { + let decl = decls[i]; + if (!decl.exported) { + continue; + }; + + match (decl.decl) { + f: ast::decl_func => append(sorted.funcs, decl), + t: []ast::decl_type => + for (let j = 0z; j < len(t); j += 1) { + append(sorted.types, ast::decl { + exported = true, + loc = decl.loc, + decl = { + // XXX: Kind of bad + let new: []ast::decl_type = []; + append(new, t[j]); + new; + }, + docs = decl.docs, + }); + }, + g: []ast::decl_global => + for (let j = 0z; j < len(g); j += 1) { + append(sorted.globals, ast::decl { + exported = true, + loc = decl.loc, + decl = { + // XXX: Kind of bad + let new: []ast::decl_global = []; + append(new, g[j]); + new; + }, + docs = decl.docs, + }); + }, + }; + }; + + sort::sort(sorted.types, size(ast::decl), &decl_cmp); + sort::sort(sorted.globals, size(ast::decl), &decl_cmp); + sort::sort(sorted.funcs, size(ast::decl), &decl_cmp); + return sorted; +}; + +fn decl_cmp(a: const *void, b: const *void) int = { + const a = *(a: const *ast::decl); + const b = *(b: const *ast::decl); + if (a.docs == "" && b.docs != "") { + return 1; + } else if (a.docs != "" && b.docs == "") { + return -1; + }; + const id_a = decl_ident(a), id_b = decl_ident(b); + return ascii::strcmp(id_a[len(id_a) - 1], id_b[len(id_b) - 1]) as int; +}; + +fn decl_ident(decl: ast::decl) ast::ident = match (decl.decl) { + f: ast::decl_func => f.ident, + t: []ast::decl_type => { + assert(len(t) == 1); + t[0].ident; + }, + g: []ast::decl_global => { + assert(len(g) == 1); + g[0].ident; + }, +}; diff --git a/cmd/haredoc/unparse.ha b/cmd/haredoc/unparse.ha @@ -1,90 +0,0 @@ -use io; -use fmt; -use hare::ast; -use hare::lex; -use hare::unparse; -use strio; - -// Forked from [hare::unparse] to reformat the output in a more suitable form -// for documentation. -fn unparse_decl(out: *io::stream, d: ast::decl) (size | io::error) = { - let n = 0z; - match (d.decl) { - g: []ast::decl_global => { - n += fmt::fprint(out, - if (g[0].is_const) "def " else "let ")?; - for (let i = 0z; i < len(g); i += 1) { - if (len(g[i].symbol) != 0) { - n += fmt::fprintf(out, - "@symbol(\"{}\") ", g[i].symbol)?; - }; - n += unparse::ident(out, g[i].ident)?; - n += fmt::fprint(out, ": ")?; - n += unparse::_type(out, 0, g[i]._type)?; - if (i + 1 < len(g)) { - n += fmt::fprint(out, ", ")?; - }; - }; - }, - t: []ast::decl_type => { - n += fmt::fprint(out, "type ")?; - for (let i = 0z; i < len(t); i += 1) { - n += unparse::ident(out, t[i].ident)?; - n += fmt::fprint(out, " = ")?; - n += unparse::_type(out, 0, t[i]._type)?; - if (i + 1 < len(t)) { - n += fmt::fprint(out, ", ")?; - }; - }; - }, - f: ast::decl_func => { - n += fmt::fprint(out, switch (f.attrs) { - ast::fndecl_attrs::NONE => "", - ast::fndecl_attrs::FINI => "@fini ", - ast::fndecl_attrs::INIT => "@init ", - ast::fndecl_attrs::TEST => "@test ", - })?; - let p = f.prototype._type as ast::func_type; - if (p.attrs & ast::func_attrs::NORETURN != 0) { - n += fmt::fprint(out, "@noreturn ")?; - }; - if (len(f.symbol) != 0) { - n += fmt::fprintf(out, "@symbol(\"{}\") ", - f.symbol)?; - }; - n += fmt::fprint(out, "fn ")?; - n += unparse::ident(out, f.ident)?; - n += prototype(out, 0, f.prototype._type as ast::func_type)?; - }, - }; - n += fmt::fprint(out, ";")?; - return n; -}; - -fn prototype( - out: *io::stream, - indent: size, - t: ast::func_type, -) (size | io::error) = { - let n = 0z; - n += fmt::fprint(out, "(")?; - for (let i = 0z; i < len(t.params); i += 1) { - let param = t.params[i]; - n += fmt::fprintf(out, "{}: ", - if (len(param.name) == 0) "_" else param.name)?; - n += unparse::_type(out, indent, *param._type)?; - if (i + 1 == len(t.params) - && t.variadism == ast::variadism::HARE) { - n += fmt::fprintf(out, "...")?; - }; - if (i + 1 < len(t.params)) { - n += fmt::fprint(out, ", ")?; - }; - }; - if (t.variadism == ast::variadism::C) { - n += fmt::fprint(out, ", ...")?; - }; - n += fmt::fprint(out, ") ")?; - n += unparse::_type(out, indent, *t.result)?; - return n; -}; diff --git a/format/html/escape.ha b/format/html/escape.ha @@ -5,7 +5,7 @@ use strio; // Prints a string to an output stream, escaping any of HTML's reserved // characters. -export fn escape(in: str, out: *io::stream) (size | io::error) = { +export fn escape(out: *io::stream, in: str) (size | io::error) = { let z = 0z; let iter = strings::iter(in); for (true) match (strings::next(&iter)) { @@ -25,16 +25,16 @@ export fn escape(in: str, out: *io::stream) (size | io::error) = { @test fn escape() void = { let sink = strio::dynamic(); defer io::close(sink); - escape("hello world!", sink); + escape(sink, "hello world!"); assert(strio::string(sink) == "hello world!"); let sink = strio::dynamic(); defer io::close(sink); - escape("\"hello world!\"", sink); + escape(sink, "\"hello world!\""); assert(strio::string(sink) == "&quot;hello world!&quot;"); let sink = strio::dynamic(); defer io::close(sink); - escape("<hello & 'world'!>", sink); + escape(sink, "<hello & 'world'!>"); assert(strio::string(sink) == "&lt;hello &amp; &apos;world&apos;!&gt;"); };