extends RefCounted const UpdaterConfig = preload("updater_config.gd") var config = UpdaterConfig.get_user_config() const FONT_H1 := 42 const FONT_H2 := 32 const FONT_H3 := 26 const FONT_H4 := 22 const FONT_H5 := 16 const FONT_H6 := 12 var HORIZONTAL_RULE: String = "[img=4000x2]res://addons/%s/generated/updater/assets/horizontal-line2.png[/img]\n" % config.plugin_name const HEADER_RULE := "[font_size=%d]$1[/font_size]\n" const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]\n" const image_download_folder := "res://cached/updater/" const exclude_font_size := "\b(?!(?:(font_size))\b)" var md_replace_patterns := [ # horizontal rules [regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE], [regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE], [regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE], # headers [regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6], [regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5], [regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4], [regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3], [regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], [regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], [regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1], [regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2], # html headers [regex("

((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], [regex("((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], [regex("

((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], [regex("((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], [regex("

((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3], [regex("((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3], [regex("

((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4], [regex("((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4], [regex("
((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5], [regex("((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5], [regex("
((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6], [regex("((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6], # asterics #[regex("(\\*)"), "xxx$1xxx"], # extract/compile image references [regex("!\\[(.*?)\\]\\[(.*?)\\]"), Callable(self, "process_image_references")], # extract images with path and optional tool tip [regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), Callable(self, "process_image")], # links [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"), "[url={\"url\":\"$3\"}]$2[/url]"], # links with tool tip [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"), "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"], # embeded text [regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/%s/generated/updater/assets/embedded.png[/img][i]$1[/i]" % config.plugin_name], # italic + bold font [regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"], [regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"], # bold font [regex("(.*?)<\\/b>"), "[b]$1[/b]"], [regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"], [regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"], # italic font [regex("(.*?)<\\/i>"), "[i]$1[/i]"], [regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"], [regex("\\*(.*?)\\*"), "[i]$1[/i]"], # strikethrough font [regex("(.*?)"), "[s]$1[/s]"], [regex("~~(.*?)~~"), "[s]$1[/s]"], [regex("~(.*?)~"), "[s]$1[/s]"], # handling lists # using an image for dots as workaroud because list is not supported checked Godot 3.x [regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)], [regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)], [regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)], [regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)], [regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)], # code blocks, code blocks looks not like code blocks in richtext [regex("```(javascript|python|shell|gdscript)([\\s\\S]*?\n)```"), code_block("$2", true)], [regex("``([\\s\\S]*?)``"), code_block("$1")], [regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")], ] var _img_replace_regex := RegEx.new() var _image_urls := PackedStringArray() var _on_table_tag := false var _client func regex(pattern :String) -> RegEx: var regex_ := RegEx.new() var err = regex_.compile(pattern) if err != OK: push_error("error '%s' checked pattern '%s'" % [err, pattern]) return null return regex_ func _init(): _img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]") func set_http_client(client) -> void: _client = client func _notification(what): if what == NOTIFICATION_PREDELETE: # finally remove_at the downloaded images for image in _image_urls: DirAccess.remove_absolute(image) DirAccess.remove_absolute(image + ".import") func list_replace(indent :int) -> String: var replace_pattern: String = ("[img=12x12]res://addons/%s/generated/updater/assets/dot2.png[/img]" % config.plugin_name) if indent %2 else ("[img=12x12]res://addons/%s/generated/updater/assets/dot1.png[/img]" % config.plugin_name) replace_pattern += " $1" for index in indent: replace_pattern = replace_pattern.insert(0, " ") return replace_pattern func code_block(replace :String, border :bool = false) -> String: var cb := "[code][color=aqua][font_size=16]%s[/font_size][/color][/code]" % replace if border: return ("[img=1400x14]res://addons/%s/generated/updater/assets/border_top.png[/img]" % config.plugin_name)\ + "[indent]" + cb + "[/indent]"\ + ("[img=1400x14]res://addons/%s/generated/updater/assets/border_bottom.png[/img]\n" % config.plugin_name) return cb func to_bbcode(input :String) -> String: input = process_tables(input) for pattern in md_replace_patterns: var regex_ :RegEx = pattern[0] var bb_replace = pattern[1] if bb_replace is Callable: input = await bb_replace.call(regex_, input) else: input = regex_.sub(input, bb_replace, true) return input + "\n" func process_tables(input :String) -> String: var bbcode := Array() var lines := Array(input.split("\n")) while not lines.is_empty(): if is_table(lines[0]): bbcode.append_array(parse_table(lines)) continue bbcode.append(lines.pop_front()) return "\n".join(PackedStringArray(bbcode)) class Table: var _columns :int var _rows := Array() class Row: var _cells := PackedStringArray() func _init(cells :PackedStringArray,columns :int): _cells = cells for i in range(_cells.size(), columns): _cells.append("") func to_bbcode(cell_sizes :PackedInt32Array, bold :bool) -> String: var cells := PackedStringArray() for cell_index in _cells.size(): var cell :String = _cells[cell_index] if cell.strip_edges() == "--": cell = create_line(cell_sizes[cell_index]) if bold: cell = "[b]%s[/b]" % cell cells.append("[cell]%s[/cell]" % cell) return "|".join(cells) func create_line(length :int) -> String: var line := "" for i in length: line += "-" return line func _init(columns :int): _columns = columns func parse_row(line :String) -> bool: # is line containing cells? if line.find("|") == -1: return false _rows.append(Row.new(line.split("|"), _columns)) return true func calculate_max_cell_sizes() -> PackedInt32Array: var cells_size := PackedInt32Array() for column in _columns: cells_size.append(0) for row_index in _rows.size(): var row :Row = _rows[row_index] for cell_index in row._cells.size(): var cell_size :int = cells_size[cell_index] var size := row._cells[cell_index].length() if size > cell_size: cells_size[cell_index] = size return cells_size func to_bbcode() -> PackedStringArray: var cell_sizes := calculate_max_cell_sizes() var bb_code := PackedStringArray() bb_code.append("[table=%d]" % _columns) for row_index in _rows.size(): bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0)) bb_code.append("[/table]\n") return bb_code func parse_table(lines :Array) -> PackedStringArray: var line :String = lines[0] var table := Table.new(line.count("|") + 1) while not lines.is_empty(): line = lines.pop_front() if not table.parse_row(line): break return table.to_bbcode() func is_table(line :String) -> bool: return line.find("|") != -1 func open_table(line :String) -> String: _on_table_tag = true return "[table=%d]" % (line.count("|") + 1) func close_table() -> String: _on_table_tag = false return "[/table]" func extract_cells(line :String, bold := false) -> String: var cells := "" for cell in line.split("|"): if bold: cell = "[b]%s[/b]" % cell cells += "[cell]%s[/cell]" % cell return cells func process_image_references(p_regex :RegEx, p_input :String) -> String: # exists references? var matches := p_regex.search_all(p_input) if matches.is_empty(): return p_input # collect image references and remove_at it var references := Dictionary() var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?") # create copy of original source to replace checked it var input := p_input.replace("\r", "") var extracted_references := p_input.replace("\r", "") for reg_match in link_regex.search_all(input): var line = reg_match.get_string(0) + "\n" var ref = reg_match.get_string(1) #var topl_tip = reg_match.get_string(4) # collect reference and url references[ref] = reg_match.get_string(2) extracted_references = extracted_references.replace(line, "") # replace image references by collected url's for reference_key in references.keys(): var regex_key := regex("\\](\\[%s\\])" % reference_key) for reg_match in regex_key.search_all(extracted_references): var ref :String = reg_match.get_string(0) var image_url :String = "](%s)" % references.get(reference_key) extracted_references = extracted_references.replace(ref, image_url) return extracted_references func process_image(p_regex :RegEx, p_input :String) -> String: var to_replace := PackedStringArray() var tool_tips := PackedStringArray() # find all matches var matches := p_regex.search_all(p_input) if matches.is_empty(): return p_input for reg_match in matches: # grap the parts to replace and store temporay because a direct replace will distort the offsets to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0))) # grap optional tool tips tool_tips.append(reg_match.get_string(5)) # finally replace all findings for replace in to_replace: var re := p_regex.sub(replace, "[img]$2[/img]") p_input = p_input.replace(replace, re) return await _process_external_image_resources(p_input) func _process_external_image_resources(input :String) -> String: DirAccess.make_dir_recursive_absolute(image_download_folder) # scan all img for external resources and download it for value in _img_replace_regex.search_all(input): if value.get_group_count() >= 1: var image_url :String = value.get_string(1) # if not a local resource we need to download it if image_url.begins_with("http"): if OS.is_stdout_verbose(): prints("download image:", image_url) var response = await _client.request_image(image_url) if response.code() == 200: var image = Image.new() var error = image.load_png_from_buffer(response.body()) if error != OK: prints("Error creating image from response", error) # replace characters where format characters var new_url := image_download_folder + image_url.get_file().replace("_", "-") if new_url.get_extension() != 'png': new_url = new_url + '.png' var err := image.save_png(new_url) if err: push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)]) _image_urls.append(new_url) input = input.replace(image_url, new_url) return input