340 lines
12 KiB
GDScript
340 lines
12 KiB
GDScript
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("<h1>((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1],
|
|
[regex("<h1[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
|
|
[regex("<h2>((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2],
|
|
[regex("<h2[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
|
|
[regex("<h3>((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3],
|
|
[regex("<h3[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3],
|
|
[regex("<h4>((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4],
|
|
[regex("<h4[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4],
|
|
[regex("<h5>((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5],
|
|
[regex("<h5[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5],
|
|
[regex("<h6>((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6],
|
|
[regex("<h6[ ]*align[ ]*=[ ]*\"center\">((.*?\\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>"), "[b]$1[/b]"],
|
|
[regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"],
|
|
[regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"],
|
|
# italic font
|
|
[regex("<i>(.*?)<\\/i>"), "[i]$1[/i]"],
|
|
[regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"],
|
|
[regex("\\*(.*?)\\*"), "[i]$1[/i]"],
|
|
|
|
# strikethrough font
|
|
[regex("<s>(.*?)</s>"), "[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
|