diff --git a/Markdown.lua b/Markdown.luau similarity index 57% rename from Markdown.lua rename to Markdown.luau index a24cd85..b43f780 100644 --- a/Markdown.lua +++ b/Markdown.luau @@ -1,3 +1,5 @@ +--!strict + ------------------------------------------------------------------------------------------------------------------------ -- Name: Markdown.lua -- Version: 1.0 (1/17/2021) @@ -15,49 +17,37 @@ local Markdown = {} -- Text Parser ------------------------------------------------------------------------------------------------------------------------ -local InlineType = { +local InlineType: {[string]: number} = { Text = 0, Ref = 1, } -local ModifierType = { +local ModifierType: {[string]: number} = { Bold = 0, Italic = 1, Strike = 2, Code = 3, } -local function sanitize(s) +local function sanitize(s: string): string return s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub("\"", """):gsub("'", "'") end -local function characters(s) - return s:gmatch(".") -end - -local function last(t) - return t[#t] -end - -local function getModifiers(stack) - local modifiers = {} - for _, modifierType in pairs(stack) do - modifiers[modifierType] = true - end - return modifiers -end +type tokenIterator = { flag: boolean, text: string } -local function parseModifierTokens(md) - local index = 1 - return function () - local text, newIndex = md:match("^([^%*_~`]+)()", index) - if text then - index = newIndex - return false, text +local function parseModifierTokens(md: string): () -> tokenIterator? + local index: number = 1 + return function (): tokenIterator? + local text: string?, textIndex: number? = md:match("^([^%*_~`]+)()", index) + if text and textIndex then + index = textIndex + return {flag=false, text=text} elseif index <= md:len() then - local text, newIndex = md:match("^(%" .. md:sub(index, index) .. "+)()", index) - index = newIndex - return true, text + local modifier: string, modifierIndex: any = md:match("^(%" .. md:sub(index, index) .. "+)()", index) + index = modifierIndex :: number + return {flag=true, text=modifier} + else + return nil end end end @@ -66,20 +56,20 @@ local function parseText(md) end -local richTextLookup = { +local richTextLookup: {[string]: number} = { ["*"] = ModifierType.Bold, ["_"] = ModifierType.Italic, ["~"] = ModifierType.Strike, ["`"] = ModifierType.Code, } -local function getRichTextModifierType(symbols) +local function getRichTextModifierType(symbols: string): number return richTextLookup[symbols:sub(1, 1)] end -local function richText(md) +local function richText(md: string): string md = sanitize(md) - local tags = { + local tags: {[number]: {[number]: string}} = { [ModifierType.Bold] = {"", ""}, [ModifierType.Italic] = {"", ""}, [ModifierType.Strike] = {"", ""}, @@ -87,25 +77,25 @@ local function richText(md) } local state = {} local output = "" - for token, text in parseModifierTokens(md) do - if token then - local modifierType = getRichTextModifierType(text) + for token in parseModifierTokens(md) do + if token.flag then + local modifierType = getRichTextModifierType(token.text) if state[ModifierType.Code] and modifierType ~= ModifierType.Code then - output = output .. text + output = output .. token.text continue end local symbolState = state[modifierType] if not symbolState then output = output .. tags[modifierType][1] - state[modifierType] = text - elseif text == symbolState then + state[modifierType] = token.text + elseif token.text == symbolState then output = output .. tags[modifierType][2] state[modifierType] = nil else - output = output .. text + output = output .. token.text end else - output = output .. text + output = output .. token.text end end for modifierType in pairs(state) do @@ -118,7 +108,7 @@ end -- Document Parser ------------------------------------------------------------------------------------------------------------------------ -local BlockType = { +local BlockType: {[string]: number} = { None = 0, Paragraph = 1, Heading = 2, @@ -128,7 +118,7 @@ local BlockType = { Quote = 6, } -local CombinedBlocks = { +local CombinedBlocks: {[number]: boolean} = { [BlockType.None] = true, [BlockType.Paragraph] = true, [BlockType.Code] = true, @@ -136,69 +126,71 @@ local CombinedBlocks = { [BlockType.Quote] = true, } -local function cleanup(s) +local function cleanup(s: string): string return s:gsub("\t", " ") end -local function getTextWithIndentation(line) - local indent, text = line:match("^%s*()(.*)") - return text, math.floor(indent / 2) +local function getTextWithIndentation(line: string): (string, number) + local indent: number?, text: string? = line:match("^%s*()(.*)") + return text :: string, math.floor(indent :: number / 2) end -- Iterator: Iterates the string line-by-line -local function lines(s) +local function lines(s: string): () -> string | nil return (s .. "\n"):gmatch("(.-)\n") end -- Iterator: Categorize each line and allows iteration -local function blockLines(md) + +local function blockLines(md: string): () -> (number | nil, string | nil) local blockType = BlockType.None local nextLine = lines(md) - local function it() + local function it(): (number | nil, string | nil) local line = nextLine() - if not line then - return - end - -- Code - if blockType == BlockType.Code then - if line:match("^```") then - blockType = BlockType.None + if line then + -- Code + if blockType == BlockType.Code then + if line:match("^```") then + blockType = BlockType.None + end + return BlockType.Code, line end - return BlockType.Code, line - end - -- Blank line - if line:match("^%s*$") then - return BlockType.None, "" - end - -- Ruler - if line:match("^%-%-%-+") or line:match("^===+") then - return BlockType.Ruler, "" - end - -- Heading - if line:match("^#") then - return BlockType.Heading, line - end - -- Code - if line:match("^%s*```") then - blockType = BlockType.Code - return blockType, line - end - -- Quote - if line:match("^%s*>") then - return BlockType.Quote, line - end - -- List - if line:match("^%s*%-%s+") or line:match("^%s*%*%s+") or line:match("^%s*[%u%d]+%.%s+") or line:match("^%s*%+%s+") then - return BlockType.List, line + -- Blank line + if line:match("^%s*$") then + return BlockType.None, "" + end + -- Ruler + if line:match("^%-%-%-+") or line:match("^===+") then + return BlockType.Ruler, "" + end + -- Heading + if line:match("^#") then + return BlockType.Heading, line + end + -- Code + if line:match("^%s*```") then + blockType = BlockType.Code + return blockType, line + end + -- Quote + if line:match("^%s*>") then + return BlockType.Quote, line + end + -- List + if line:match("^%s*%-%s+") or line:match("^%s*%*%s+") or line:match("^%s*[%u%d]+%.%s+") or line:match("^%s*%+%s+") then + return BlockType.List, line + end + -- Paragraph + return BlockType.Paragraph, line -- should take into account indentation of first-line + else + return nil end - -- Paragraph - return BlockType.Paragraph, line -- should take into account indentation of first-line end return it end -- Iterator: Joins lines of the same type into a single element -local function textBlocks(md) +local function textBlocks(md: string): () -> (number, string) local it = blockLines(md) local lastBlockType, lastLine = it() return function () @@ -223,9 +215,9 @@ local function textBlocks(md) end -- Iterator: Transforms raw blocks into sections with data -local function blocks(md, markup) +local function blocks(md: string, markup: (string) -> string) local nextTextBlock = textBlocks(md) - local function it() + local function it(): (number, {[string]: any}) local blockType, blockText = nextTextBlock() if blockType == BlockType.None then return it() -- skip this block type @@ -237,27 +229,33 @@ local function blocks(md, markup) if blockType == BlockType.Paragraph then block.Text = markup(text) elseif blockType == BlockType.Heading then - local level, text = blockText:match("^#+()%s*(.*)") - block.Level, block.Text = level - 1, markup(text) + local level: number?, heading: string? = blockText:match("^#+()%s*(.*)") + if level and heading then + block.Level, block.Text = level - 1, markup(heading) + end elseif blockType == BlockType.Code then - local syntax, code = text:match("^```(.-)\n(.*)\n```$") - block.Syntax, block.Code = syntax, syntax == "raw" and code or sanitize(code) + local syntax: string?, code: string? = text:match("^```(.-)\n(.*)\n```$") + if syntax and code then + block.Syntax, block.Code = syntax, syntax == "raw" and code or sanitize(code) + end elseif blockType == BlockType.List then - local lines = blockText:split("\n") + local lines: {[number]: any} = blockText:split("\n") for i, line in ipairs(lines) do - local text, indent = getTextWithIndentation(line) - local symbol, text = text:match("^(.-)%s+(.*)") - lines[i] = { - Level = indent, - Text = markup(text), - Symbol = symbol, - } + local lineText: string, lineIndent: number = getTextWithIndentation(line) + local symbol: string?, item: string? = lineText:match("^(.-)%s+(.*)") + if symbol and item then + lines[i] = { + Level = lineIndent, + Text = markup(item), + Symbol = symbol, + } + end end block.Lines = lines elseif blockType == BlockType.Quote then - local lines = blockText:split("\n") + local lines: {[number]: string} = blockText:split("\n") for i = 1, #lines do - lines[i] = lines[i]:match("^%s*>%s*(.*)") + lines[i] = lines[i]:match("^%s*>%s*(.*)") :: string end local rawText = table.concat(lines, "\n") block.RawText, block.Iterator = rawText, blocks(rawText, markup) @@ -265,10 +263,10 @@ local function blocks(md, markup) end return blockType, block end - return it + return it end -local function parseDocument(md, inlineParser) +local function parseDocument(md: string, inlineParser: any) return blocks(cleanup(md), inlineParser or richText) end