all repos — hastyscribe @ f6b018694ff025e41c9c4351e8a19b1ebdbe8c28

A professional markdown compiler.

Merge pull request #90 from ZoomRmc/clifixes

Fabio Cevasco h3rald@h3rald.com
Mon, 18 Sep 2023 16:02:33 -0400
commit

f6b018694ff025e41c9c4351e8a19b1ebdbe8c28

parent

089992a9cdf5f1bfc8746077dcc5836e6643d3dd

M src/hastyscribe.nimsrc/hastyscribe.nim

@@ -1,22 +1,24 @@

-import - std/macros, - std/os, - std/parseopt, - std/strutils, - std/sequtils, - std/times, - std/pegs, - std/xmltree, - std/tables, - std/httpclient, - std/logging +import std/[ + macros, + os, + parseopt, + strutils, + times, + pegs, + xmltree, + tables, + httpclient, + logging, + critbits, + ] from nimquery import querySelectorAll from std/htmlparser import parseHtml +from std/sequtils import mapIt import hastyscribepkg/niftylogger, - hastyscribepkg/markdown, + hastyscribepkg/markdown, hastyscribepkg/config, hastyscribepkg/consts, hastyscribepkg/utils

@@ -24,7 +26,7 @@

export consts -when defined(windows) and defined(amd64): +when defined(windows) and defined(amd64): {.passL: "-static -L"&getProjectPath()&"/hastyscribepkg/vendor/markdown/windows -lmarkdown".} elif defined(linux) and defined(amd64): {.passL: "-static -L"&getProjectPath()&"/hastyscribepkg/vendor/markdown/linux -lmarkdown".}

@@ -34,14 +36,18 @@

type HastyOptions* = object - toc*: bool - input*: string - output*: string - css*: string - js*: string + toc*: bool = true + input*: string = "" + output*: string = "" + css*: string = "" + js*: string = "" watermark*: string - fragment*: bool - embed*: bool + fragment*: bool = false + embed*: bool = true + iso*: bool = false + noclobber*: bool = false + outputToDir*: bool = false + processingMultiple: bool = false HastyFields* = Table[string, string] HastySnippets* = Table[string, string] HastyMacros* = Table[string, string]

@@ -116,16 +122,15 @@ var imgformat = imgfile.image_format

if imgformat == "svg": imgformat = "svg+xml" var imgcontent = "" - if imgfile.startsWith(peg"'data:'"): + if imgfile.startsWith(peg"'data:'"): continue elif imgfile.startsWith(peg"'http' 's'? '://'"): try: let client = newHttpClient() imgcontent = encode_image(client.getContent(imgfile), imgformat) - except CatchableError: - warn "Unable to download '" & imgfile & "'" - warn " Reason: " & getCurrentExceptionMsg() - warn " -> Image will be linked instead" + except CatchableError as e: + warn "Unable to download '$1'\n Reason: $2\n" % [imgfile, e.msg] & + " -> Image will be linked instead" continue else: imgcontent = encode_image_file(current_dir & imgfile, imgformat)

@@ -324,29 +329,18 @@ else:

warn "Snippet '" & id & "' not defined." result = result.replace(snippet, "") -# Substitute escaped brackets or hashes *after* preprocessing proc remove_escapes(hs: var HastyScribe, document: string): string = - result = document - for lb in document.findAll(peg"'\\{'"): - result = result.replace(lb, "{") - for rb in document.findAll(peg"'\\}'"): - result = result.replace(rb, "}") - for h in document.findAll(peg"'\\#'"): - result = result.replace(h, "#") + ## Substitute escaped brackets or hashes *after* preprocessing + document.replacef(peg"'\\' {'{' / '}' / '#'}", "$1") proc parse_anchors(hs: var HastyScribe, document: string): string = - result = document let peg_anchor = peg""" - anchor <- \s '#' {id} '#' + anchor <- \s '#' {id} '#' id <- [a-zA-Z][a-zA-Z0-9:._-]+ """ - for anchor in document.findAll(peg_anchor): - var matches:array[0..0, string] - discard anchor.match(peg_anchor, matches) - var id = matches[0] - result = result.replace(anchor, " <a id=\""&id&"\"></a>") + document.replacef(peg_anchor, """ <a id="$1"></a>""") -proc preprocess*(hs: var HastyScribe, document, dir: string, offset = 0): string = +proc preprocess*(hs: var HastyScribe, document, dir: string, offset = 0): string = result = hs.parse_transclusions(document, dir, offset) result = hs.parse_fields(result) result = hs.parse_snippets(result)

@@ -361,53 +355,42 @@ except CatchableError:

warn obj & " not found: " & key proc create_optional_css*(hs: HastyScribe, document: string): string = - result = "" - let html = document.parseHtml + let html = document.parseHtml() + var rules: seq[string] # Check icons - let iconRules = html.querySelectorAll("span[class^=fa-]") - .mapIt(it.attr("class")) - .mapIt(getTableValue(hs.iconStyles, it, "Icon")) - result &= iconRules.join("\n") + for icon in html.querySelectorAll("span[class^=fa-]"): + rules.add getTableValue(hs.iconStyles, icon.attr("class"), "Icon") # Check badges - let badgeRules = html.querySelectorAll("span[class^=badge-]") - .mapIt(it.attr("class")) - .mapIt(getTableValue(hs.badgeStyles, it, "Badge")) - result &= badgeRules.join("\n") + for badge in html.querySelectorAll("span[class^=badge-]"): + rules.add getTableValue(hs.badgeStyles, badge.attr("class"), "Badge") # Check notes - let noteRules = html.querySelectorAll("div.tip, div.warning, div.note, div.sidebar") - .mapIt(it.attr("class")) - .mapIt(getTableValue(hs.noteStyles, it, "Note")) - result &= noteRules.join("\n") + for note in html.querySelectorAll("div.tip, div.warning, div.note, div.sidebar"): + rules.add getTableValue(hs.noteStyles, note.attr("class"), "Note") # Check links - let linkHrefs = html.querySelectorAll("a[href]") - .mapIt(it.attr("href")) - var linkRules = newSeq[string]() + let linkHrefs = html.querySelectorAll("a[href]").mapIt(it.attr("href")) + var linkRulesSet: CritBitTree[void] # Add #document-top rule because it is always needed and added at the end. - linkRules.add hs.linkStyles["^='#document-top"] + rules.add hs.linkStyles["^='#document-top"] + for href in linkHrefs: - for key in hs.linkStyles.keys.toSeq: - if not linkRules.contains(hs.linkStyles[key]): + for (key, val) in hs.linkStyles.pairs: + if val notin linkRulesSet: let op = key[0..1] let value = key[3..^1] # Skip first ' - var matches = newSeq[string]() # Save matches in order of priority - if op == "$=" and href.endsWith(value): - matches.add key - if op == "*=" and href.contains(value): - matches.add key - if op == "^=" and href.startsWith(value): - matches.add key - # Add last match - if matches.len > 0: - linkRules.add hs.linkStyles[matches[^1]] + # TODO: debug logic + # Save matches in order of priority (?) + if (op == "$=" and href.endsWith(value)) or + (op == "*=" and href.contains(value)) or + (op == "^=" and href.startsWith(value)): + linkRulesSet.incl val + rules.add val break - result &= linkRules.join("\n") - result = result.style_tag + rules.join("\n").style_tag() # Public API proc compileFragment*(hs: var HastyScribe, input, dir: string, toc = false): string {.discardable.} = - hs.options.input = input - hs.document = hs.options.input + hs.document = input # Parse transclusions, fields, snippets, and macros hs.document = hs.preprocess(hs.document, dir) # Process markdown

@@ -418,68 +401,52 @@ hs.document = hs.document.md(flags)

return hs.document proc compileDocument*(hs: var HastyScribe, input, dir: string): string {.discardable.} = - hs.options.input = input - hs.document = hs.options.input + hs.document = input # Load style rules to be included on-demand hs.load_styles() # Parse transclusions, fields, snippets, and macros hs.document = hs.preprocess(hs.document, dir) - # Document Variables - var - main_css_tag = "" - optional_css_tag = "" - user_css_tag = "" - user_js_tag = "" - watermark_css_tag = "" - headings = " class=\"headings\"" - author_footer = "" - title_tag = "" - header_tag = "" - toc = "" - metadata = TMDMetaData(title:"", author:"", date:"", toc:"", css:"") - let logo_datauri = encode_image(hastyscribe_logo, "svg") - let hastyscribe_svg = """ - <img src="$#" width="80" height="23" alt="HastyScribe"> - """ % [logo_datauri] # Process markdown + var metadata: TMDMetaData hs.document = hs.document.md(0, metadata) - # Manage metadata - if metadata.author != "": - author_footer = "<span class=\"copy\"></span> " & metadata.author & " &ndash;" - if metadata.title != "": - title_tag = "<title>" & metadata.title & "</title>" - header_tag = "<div id=\"header\"><h1>" & metadata.title & "</h1></div>" - else: - title_tag = "" - header_tag = "" - if hs.options.toc and metadata.toc != "": - toc = "<div id=\"toc\">" & metadata.toc & "</div>" - else: - headings = "" - toc = "" - - if hs.options.css != "": - user_css_tag = hs.options.css.readFile.style_tag + # Document Variables + const hastyscribe_img = """ +<img src="$#" width="80" height="23" alt="HastyScribe"> +""" % encode_image(hastyscribe_logo, "svg") + let + (headings, toc) = if hs.options.toc and metadata.toc != "": + (" class=\"headings\"", "<div id=\"toc\">" & metadata.toc & "</div>") + else: ("", "") + user_css_tag = if hs.options.css == "": "" else: + hs.options.css.readFile.style_tag + user_js_tag = if hs.options.js == "": "" else: + "<script type=\"text/javascript\">\n" & hs.options.js.readFile & "\n</script>" + watermark_css_tag = if hs.options.watermark == "": "" else: + watermark_css(hs.options.watermark) - if hs.options.js != "": - user_js_tag = "<script type=\"text/javascript\">\n" & hs.options.js.readFile & "\n</script>" + # Manage metadata + author_footer = if metadata.author == "": "" else: + "<span class=\"copy\"></span> " & metadata.author & " &ndash;" + title_tag = if metadata.title == "": "" else: + "<title>" & metadata.title & "</title>" + header_tag = if metadata.title == "": "" else: + "<div id=\"header\"><h1>" & metadata.title & "</h1></div>" - if hs.options.watermark != "": - watermark_css_tag = watermark_css(hs.options.watermark) + (main_css_tag, optional_css_tag) = if hs.options.embed: + (stylesheet.style_tag, hs.create_optional_css(hs.document)) + else: + ("", "") # Date parsing and validation - var timeinfo: DateTime = local(getTime()) - - - try: - timeinfo = parse(metadata.date, "yyyy-MM-dd") - except CatchableError: - timeinfo = parse(getDateStr(), "yyyy-MM-dd") - - if hs.options.embed: - main_css_tag = stylesheet.style_tag - optional_css_tag = hs.create_optional_css(hs.document) + let date: string = block: + const IsoDate = initTimeFormat("yyyy-MM-dd") + const DefaultDate = initTimeFormat("MMMM d, yyyy") + let timeinfo: DateTime = try: + parse(metadata.date, IsoDate) + except CatchableError: + local(getTime()) + timeinfo.format(if hs.options.iso: IsoDate else: DefaultDate) hs.document = """<!doctype html> <html lang="en">

@@ -505,26 +472,26 @@ $body

</div> <div id="footer"> <p>$author_footer $date</p> - <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe" class="hastyscribe-logo">$hastyscribe_svg</a></p> + <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe" class="hastyscribe-logo">$hastyscribe_img</a></p> </div> </div> $js </body>""" % [ - "title_tag", title_tag, - "header_tag", header_tag, - "author", metadata.author, - "author_footer", author_footer, - "date", timeinfo.format("MMMM d, yyyy"), - "toc", toc, - "main_css_tag", main_css_tag, - "hastyscribe_svg", hastyscribe_svg, - "optional_css_tag", optional_css_tag, - "user_css_tag", user_css_tag, - "headings", headings, - "body", hs.document, - "internal_css_tag", metadata.css, - "watermark_css_tag", watermark_css_tag, - "js", user_js_tag] + "title_tag", title_tag, + "header_tag", header_tag, + "author", metadata.author, + "author_footer", author_footer, + "date", date, + "toc", toc, + "main_css_tag", main_css_tag, + "hastyscribe_img", hastyscribe_img, + "optional_css_tag", optional_css_tag, + "user_css_tag", user_css_tag, + "headings", headings, + "body", hs.document, + "internal_css_tag", metadata.css, + "watermark_css_tag", watermark_css_tag, + "js", user_js_tag] if hs.options.embed: hs.embed_images(dir) hs.document = add_jump_to_top_links(hs.document)

@@ -532,67 +499,114 @@ # Use IDs instead of names for anchors

hs.document = hs.document.replace("<a name=", "<a id=") return hs.document -proc compile*(hs: var HastyScribe, input_file: string) = - let inputsplit = input_file.splitFile - var input = input_file.readFile - var output: string +type ClobberError = object of CatchableError - if hs.options.output == "": - output = inputsplit.dir/inputsplit.name & ".htm" - else: - output = hs.options.output +proc compile(hs: var HastyScribe; input_file, out_basename: string) + {.raises: [IOError, ref ValueError, OSError, Exception, ClobberError].} = + const OutputExt = ".htm" + let + (dir, name, _) = input_file.splitFile() + input: string = input_file.readFile() + outBaseName = if out_basename != "": out_basename else: name + outputPath: string = if hs.options.output == "": + dir/outBaseName & OutputExt + else: + if hs.options.outputToDir: # explicit name is a dir + hs.options.output / outBaseName & OutputExt + else: # explicit name is a file path + hs.options.output if hs.options.fragment: - hs.compileFragment(input, inputsplit.dir) + hs.compileFragment(input, dir) else: - hs.compileDocument(input, inputsplit.dir) - if output != "-": - output.writeFile(hs.document) + hs.compileDocument(input, dir) + if outputPath == "-": + stdout.write(hs.document) + if hs.options.processingMultiple: + stdout.write("\n" & (eof_separator % ["name", name]) & "\n") else: - stdout.write(hs.document) + if fileExists(outputPath) and hs.options.noclobber: + raise newException(ClobberError, outputPath) + else: + # TODO: implement atomic writes with temp files + outputPath.writeFile(hs.document) -### MAIN +proc compile*(hs: var HastyScribe, input_file: string) + {.raises: [IOError, ref ValueError, OSError, Exception, ClobberError].} = + compile(hs, input_file, "") +proc fileNameMappings(paths: sink CritBitTree[void]): seq[tuple[path, name: string]] = + ## This function preemptively deals with potential name collisions on + ## writing multiple files to a flat output directory + ## Outputs a mapping of file paths to their unique base names + var baseNameSet: CritBitTree[(int, bool)] # (indexInMap, madeUnique) + var i = 0 + for path in paths: + let (dir, name, _) = path.splitFile() + if baseNameSet.containsOrIncl(name, (i, false)): + let (oldIdx, madeUnique) = baseNameSet[name] + if not madeUnique: # First collision, make both old and new files unique + let oldMap = result[oldIdx] + let newName = makeFNameUnique(oldMap.name, oldMap.path.splitFile.dir) + result[oldIdx] = (path: oldMap.path, name: newName) + baseNameSet[name] = (oldIdx, true) + # Subsequent name collisions, make only the new name unique + result.add (path: path, name: makeFNameUnique(name, dir)) + else: + baseNameSet.incl(name, (i, false)) + result.add (path: path, name: name) + i.inc() + +### MAIN when isMainModule: - let usage = " HastyScribe v" & pkgVersion & " - Self-contained Markdown Compiler" & """ + const usage = " HastyScribe v" & pkgVersion & " - Self-contained Markdown Compiler" & """ (c) 2013-2023 Fabio Cevasco Usage: - hastyscribe <markdown_file_or_glob> [options] + hastyscribe [options] <markdown_file_or_glob> ... Arguments: markdown_file_or_glob The markdown (or glob expression) file to compile into HTML. Options: + --output-file=<file> Write output to <file>. + (Use "--output-file=-" to output to stdout) + --output-dir=<dir>, -d=<dir> Write output files to <dir>. Overrides "output-file". + Input directory structure is not preserved. --field/<field>=<value> Define a new field called <field> with value <value>. - --notoc Do not generate a Table of Contents. --user-css=<file> Insert contents of <file> as a CSS stylesheet. --user-js=<file> Insert contents of <file> as a Javascript script. - --output-file=<file> Write output to <file>. - (Use "--output-file=-" to output to stdout) --watermark=<file> Use the image in <file> as a watermark. + --notoc Do not generate a Table of Contents. --noembed If specified, styles and images will not be embedded. - --fragment If specified, an HTML fragment will be generated, without - embedding images ir stylesheets. - --help Display the usage information.""" - + --fragment If specified, an HTML fragment will be generated, without + embedding images or stylesheets. + --iso Use ISO 8601 date format (e.g., 2000-12-31) in the footer. + --no-clobber, -n Do not overwrite existing files. + --help, -h Display the usage information. + --version, -v Print version and exit.""" - var input = "" - var files = newSeq[string](0) - var options = HastyOptions(toc: true, output: "", css: "", watermark: "", fragment: false, embed: true) - var fields = initTable[string, string]() + type ErrorKinds = enum errENOENT = 2, errEIO = 5 + + var + inputs: seq[string] + options = default(HastyOptions) + fields = initTable[string, string]() # Parse Parameters - + template noVal() = + if val != "": fatal "Option '" & key & "' takes no value"; quit(1) for kind, key, val in getopt(): case kind of cmdArgument: - input = key + inputs.add(key) of cmdShortOption, cmdLongOption: case key of "notoc": + noVal() options.toc = false of "noembed": + noVal() options.embed = false of "user-css": options.css = val

@@ -601,37 +615,86 @@ options.js = val

of "watermark": options.watermark = val of "output-file": - options.output = val + if not options.outputToDir: + if val == "": fatal "Output file path can't be empty"; quit(1) + options.output = val + of "d", "output-dir": + options.outputToDir = true + if dirExists(val): options.output = val.normalizedPath() + else: + fatal "Directory '" & val & "' does not exist"; + quit(errENOENT.ord) of "fragment": + noVal() options.fragment = true + of "iso": + noVal() + options.iso = true + of "n", "no-clobber", "noclobber": + noVal() + options.noclobber = true of "v", "version": echo pkgVersion quit(0) of "h", "help": echo usage - quit(0) + quit(0) else: if key.startsWith("field/"): let val = val - fields[key.replace("field/", "")] = val - discard - else: - discard - for file in walkFiles(input): - files.add(file) + fields[key.replace("field/", "")] = val + else: + warn """Unknown option "$#", ignoring""" % key + of cmdEnd: assert(false) + if inputs.len == 0: + echo usage + quit(0) + else: + var errorsOccurred: set[ErrorKinds] = {} + var paths: CritBitTree[void] # Deduplicates different globs expanding to same files + for glob in inputs: + var globMatchCount = 0 + for file in walkFiles(glob): + # TODO: files can still contain relative and absolute paths pointing to the same file + let path = file.normalizedPath() + if paths.containsOrIncl(path): + notice "Input file \"$1\" provided multiple times" % path + globMatchCount.inc() + if globMatchCount == 0: + errorsOccurred.incl errENOENT + fatal "\"$1\" does not match any file" % glob + if paths.len == 0: + errorsOccurred.incl errENOENT + else: + var fileMappings: seq[tuple[path, name: string]] + if paths.len > 1: + options.processingMultiple = true + if not options.outputToDir: + case options.output: + of "": discard + of "-": + notice "Multiple files will be printed to stdout using the\n" & + " \"" & eof_Separator & "\" separator." + else: + warn "Option `output-file` is set but multiple input files given, ignoring" + options.output = "" + fileMappings = fileNameMappings(paths) + else: + for p in paths.keys: + fileMappings.add (path: p, name: "") - if files.len == 0: - if input == "": - echo usage - quit(0) - fatal "\"$1\" does not match any file" % [input] - quit(2) - else: - var hs = newHastyScribe(options, fields) - try: - for file in files: - hs.compile(file) - except IOError: - let msg = getCurrentExceptionMsg() - fatal msg - quit(3) + var hs = newHastyScribe(options, fields) + for (path, outName) in fileMappings: + try: + hs.compile(path, outName) + except IOError as e: + errorsOccurred.incl errEIO + fatal e.msg + continue + except ClobberError as e: + warn "File '" & e.msg & "' exists, not overwriting" + continue + info "\"$1\" converted successfully" % path + if errENOENT in errorsOccurred: quit(errENOENT.ord) + elif errEIO in errorsOccurred: quit(errEIO.ord) + else: discard # ok
M src/hastyscribepkg/consts.nimsrc/hastyscribepkg/consts.nim

@@ -1,5 +1,5 @@

-const +const stylesheet* = "./data/hastystyles.css".slurp stylesheet_badges* = "./data/hastystyles.badges.css".slurp stylesheet_icons* = "./data/hastystyles.icons.css".slurp

@@ -26,3 +26,4 @@ background-position: center 70px;

background-attachment: fixed; } """ + eof_separator* = "<!-- $name: EOF -->"
M src/hastyscribepkg/markdown.nimsrc/hastyscribepkg/markdown.nim

@@ -1,65 +1,65 @@

-const +const MKDIO_D* = true -type +type MMIOT* = int mkd_flag_t* = cuint {.push importc, cdecl.} # line builder for markdown() -# +# proc mkd_in*(a2: ptr FILE; a3: mkd_flag_t): ptr MMIOT -# assemble input from a file +# assemble input from a file proc mkd_string*(a2: cstring; a3: cint; a4: mkd_flag_t): ptr MMIOT -# assemble input from a buffer +# assemble input from a buffer # line builder for github flavoured markdown -# +# proc gfm_in*(a2: ptr FILE; a3: mkd_flag_t): ptr MMIOT -# assemble input from a file +# assemble input from a file proc gfm_string*(a2: cstring; a3: cint; a4: mkd_flag_t): ptr MMIOT -# assemble input from a buffer +# assemble input from a buffer proc mkd_basename*(a2: ptr MMIOT; a3: cstring) proc mkd_initialize*() proc mkd_with_html5_tags*() proc mkd_shlib_destructor*() # compilation, debugging, cleanup -# +# proc mkd_compile*(a2: ptr MMIOT; a3: mkd_flag_t): cint proc mkd_cleanup*(a2: ptr MMIOT) # markup functions -# +# proc mkd_dump*(a2: ptr MMIOT; a3: ptr FILE; a4: cint; a5: cstring): cint proc markdown*(a2: ptr MMIOT; a3: ptr FILE; a4: mkd_flag_t): cint proc mkd_line*(a2: cstring; a3: cint; a4: cstringArray; a5: mkd_flag_t): cint -type +type mkd_sta_function_t* = proc (a2: cint; a3: pointer): cint -proc mkd_string_to_anchor*(a2: cstring; a3: cint; a4: mkd_sta_function_t; +proc mkd_string_to_anchor*(a2: cstring; a3: cint; a4: mkd_sta_function_t; a5: pointer; a6: cint) proc mkd_xhtmlpage*(a2: ptr MMIOT; a3: cint; a4: ptr FILE): cint # header block access -# +# proc mkd_doc_title*(a2: ptr MMIOT): cstring proc mkd_doc_author*(a2: ptr MMIOT): cstring proc mkd_doc_date*(a2: ptr MMIOT): cstring # compiled data access -# +# proc mkd_document*(a2: ptr MMIOT; a3: cstringArray): cint proc mkd_toc*(a2: ptr MMIOT; a3: cstringArray): cint proc mkd_css*(a2: ptr MMIOT; a3: cstringArray): cint proc mkd_xml*(a2: cstring; a3: cint; a4: cstringArray): cint # write-to-file functions -# +# proc mkd_generatehtml*(a2: ptr MMIOT; a3: ptr FILE): cint proc mkd_generatetoc*(a2: ptr MMIOT; a3: ptr FILE): cint proc mkd_generatexml*(a2: cstring; a3: cint; a4: ptr FILE): cint proc mkd_generatecss*(a2: ptr MMIOT; a3: ptr FILE): cint -const +const mkd_style* = mkd_generatecss proc mkd_generateline*(a2: cstring; a3: cint; a4: ptr FILE; a5: mkd_flag_t): cint -const +const mkd_text* = mkd_generateline # url generator callbacks -# -type +# +type mkd_callback_t* = proc (a2: cstring; a3: cint; a4: pointer): cstring mkd_free_t* = proc (a2: cstring; a3: pointer) proc mkd_e_url*(a2: pointer; a3: mkd_callback_t)

@@ -67,7 +67,7 @@ proc mkd_e_flags*(a2: pointer; a3: mkd_callback_t)

proc mkd_e_free*(a2: pointer; a3: mkd_free_t) proc mkd_e_data*(a2: pointer; a3: pointer) # version#. -# +# var markdown_version*: ptr char proc mkd_mmiot_flags*(a2: ptr FILE; a3: ptr MMIOT; a4: cint) proc mkd_flags_are*(a2: ptr FILE; a3: mkd_flag_t; a4: cint)

@@ -75,8 +75,8 @@ proc mkd_ref_prefix*(a2: ptr MMIOT; a3: cstring)

{.pop.} # special flags for markdown() and mkd_text() -# -const +# +const MKD_NOLINKS* = 0x00000001 MKD_NOIMAGE* = 0x00000002 MKD_NOPANTS* = 0x00000004

@@ -111,24 +111,24 @@ MKD_EMBED* = MKD_NOLINKS or MKD_NOIMAGE or MKD_TAGTEXT

## High Level API -import +import std/pegs -const - DefaultFlags = MKD_TOC or MKD_1_COMPAT or MKD_EXTRA_FOOTNOTE or MKD_DLEXTRA or MKD_FENCEDCODE or MKD_GITHUBTAGS or MKD_URLENCODEDANCHOR or MKD_LATEX +const + DefaultFlags = MKD_TOC or MKD_1_COMPAT or MKD_EXTRA_FOOTNOTE or MKD_DLEXTRA or MKD_FENCEDCODE or MKD_GITHUBTAGS or MKD_URLENCODEDANCHOR or MKD_LATEX -type TMDMetaData* = object - title*: string - author*: string - date*: string - toc*: string - css*: string +type TMDMetaData* = object + title*: string = "" + author*: string = "" + date*: string = "" + toc*: string = "" + css*: string = "" proc md*(s: string, f = 0): string = var flags: uint32 if (f == 0): flags = DefaultFlags - else: + else: flags = uint32(f) var str = cstring(s&" ") var mmiot = mkd_string(str, cint(str.len-1), flags)

@@ -139,11 +139,12 @@ result = cstringArrayToSeq(res)[0]

mkd_cleanup(mmiot) return -proc md*(s: string, f = 0, data: var TMDMetadata): string = +proc md*(s: string, f = 0; data: out TMDMetaData): string = + data = default(TMDMetaData) var flags: uint32 if (f == 0): flags = DefaultFlags - else: + else: flags = uint32(f) # Check if Pandoc style metadata is present var valid_metadata = false

@@ -152,7 +153,7 @@ let peg_pandoc = peg"""

definition <- ^{line} {line}? {line}? line <- '\%' @ \n """ - var matches: array[0..2, string] + var matches: array[0..2, string] let (s, e) = contents.findBounds(peg_pandoc, matches) # the pattern must start at the beginning of the file if s == 0:

@@ -160,7 +161,7 @@ if matches[0] != "" and matches[1] != "" and matches[2] != "":

valid_metadata = true else: # incomplete metadata, remove the whole pandoc section to not confuse discount - contents = contents[e-1 .. ^1] + contents = contents[e-1 .. ^1] var str = cstring(contents) var mmiot = mkd_string(str, cint(str.len), flags) if valid_metadata:

@@ -173,18 +174,15 @@ if (int(flags) and MKD_TOC) == MKD_TOC:

var toc = allocCStringArray(@[""]) if mkd_toc(mmiot, toc) > 0: data.toc = cstringArrayToSeq(toc)[0] - else: - data.toc = "" # Process CSS - var css = allocCStringArray(newSeq[string](10)) - if mkd_css(mmiot, css) > 0: - data.css = cstringArrayToSeq(css)[0] - else: - data.css = "" + data.css = block: + var css = allocCStringArray(newSeq[string](10)) + if mkd_css(mmiot, css) > 0: cstringArrayToSeq(css)[0] + else: "" # Generate HTML - var res = allocCStringArray([""]) - if mkd_document(mmiot, res) > 0: - result = cstringArrayToSeq(res)[0] - else: - result = "" + let html = block: + var res = allocCStringArray([""]) + if mkd_document(mmiot, res) > 0: cstringArrayToSeq(res)[0] + else: "" mkd_cleanup(mmiot) + html
M src/hastyscribepkg/niftylogger.nimsrc/hastyscribepkg/niftylogger.nim

@@ -1,13 +1,14 @@

-import - std/logging, - std/strutils, - std/terminal, - std/exitprocs +import std/[ + logging, + strutils, + terminal, + exitprocs, + ] if isatty(stdin): addExitProc(resetAttributes) -type +type NiftyLogger* = ref object of Logger proc logPrefix*(level: Level): tuple[msg: string, color: ForegroundColor] =

@@ -17,7 +18,7 @@ return ("---", fgMagenta)

of lvlInfo: return ("(i)", fgCyan) of lvlNotice: - return (" ", fgWhite) + return (" ", fgBlue) of lvlWarn: return ("(!)", fgYellow) of lvlError:

@@ -25,12 +26,12 @@ return ("(!)", fgRed)

of lvlFatal: return ("(x)", fgRed) else: - return (" ", fgWhite) + return (" ", fgDefault) method log*(logger: NiftyLogger; level: Level; args: varargs[string, `$`]) = var f = stdout if level >= getLogFilter() and level >= logger.levelThreshold: - if level >= lvlWarn: + if level >= lvlWarn: f = stderr let ln = substituteLog(logger.fmtStr, level, args) let prefix = level.logPrefix()
M src/hastyscribepkg/utils.nimsrc/hastyscribepkg/utils.nim

@@ -1,32 +1,35 @@

-import - std/base64, - std/os, - std/strutils, - std/pegs +import std/[ + base64, + os, + strutils, + pegs, + hashes, + ] import consts -proc style_tag*(css: string): string = - result = "<style>$1</style>" % [css] +template style_tag*(css: string): string = + "<style>" & css & "</style>" proc style_link_tag*(css: string): string = result = "<link rel=\"stylesheet\" href=\"$1\"/>" % [css] proc encode_image*(contents, format: string): string = - if format == "svg": - let encoded_svg = contents - .replace("\"", "'") - .replace("%", "%25") - .replace("#", "%23") - .replace("{", "%7B") - .replace("}", "%7D") - .replace("<", "%3C") - .replace(">", "%3E") - .replace(" ", "%20") - return "data:image/svg+xml,$#" % [encoded_svg] - else: - return "data:image/$format;base64,$enc_contents" % ["format", format, "enc_contents", contents.encode] + if format == "svg": + let encoded_svg = contents.multireplace([ + ("\"", "'"), + ("%", "%25"), + ("#", "%23"), + ("{", "%7B"), + ("}", "%7D"), + ("<", "%3C"), + (">", "%3E"), + (" ", "%20"), + ]) + "data:image/svg+xml,$#" % [encoded_svg] + else: + "data:image/$format;base64,$enc_contents" % ["format", format, "enc_contents", contents.encode] proc encode_image_file*(file, format: string): string = if (file.fileExists):

@@ -49,3 +52,14 @@ result = (watermark_style % [img]).style_tag

proc add_jump_to_top_links*(document: string): string = result = document.replacef(peg"{'</h' [23456] '>'}", "<a href=\"#document-top\" title=\"Go to top\"></a>$1") + +proc makeFNameUnique*(baseName, dir: string): string = + ## Uses file placement (`dir`) as a unique name identifier + ## Files in relative root (`dir` is empty) are returned unchanged. + if dir notin ["", ".", "./"]: + let + dir = when dosLikeFileSystem: dir.replace('\\', '/') else: dir + hashBytes = cast[array[sizeof(Hash), byte]](hash(dir)) + uniquePrefix = encode(hashBytes, safe=true) + baseName & '_' & uniquePrefix + else: baseName