Skip to content

Commit

Permalink
Merge pull request #6 from daenvil/links-dev
Browse files Browse the repository at this point in the history
Link implementation
  • Loading branch information
daenvil committed Dec 16, 2023
2 parents a123b55 + b2fc0ba commit 1e36d5b
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 36 deletions.
26 changes: 15 additions & 11 deletions addons/markdownlabel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ My initial use case that lead me to do this was to directly include text from fi
Simply add a MarkdownLabel to the scene and write its `markdown_text` field in Markdown format.

In the RichTextLabel properties:
- **`bbcode_enabled` property must be enabled**.
- Do not touch the `text` property, since it's internally used by MarkdownLabel to properly format its text.
- Do not touch neither the `bbcode_enabled` nor the `text` property, since they are internally used by MarkdownLabel to properly format its text. Both properties are hidden from the editor to prevent mistakenly editing them.
- You can use the rest of its properties as normal.

You can still use BBCode tags that don't have a Markdown equivalent, such as `[color=green]underlined text[/color]`, allowing you to have the full functionality of RichTextLabel with the simplicity and readibility of Markdown.
Expand All @@ -58,7 +57,6 @@ You can still use BBCode tags that don't have a Markdown equivalent, such as `[c
### Basic syntax

The basic Markdown syntax works in the standard way:

```
Markdown text ................ -> BBCode equivalent
-------------------------------||------------------
Expand Down Expand Up @@ -92,7 +90,7 @@ multiline codeblock .......... -> multiline codeblock

MarkdownLabel supports headers, although RichTextLabel doesn't. By default, a line defined as a header will have its font size scaled by a pre-defined amount.

To define a line as a header, begin it with any number of consecutive hash symbols (#) and follow it with the title of your header. The number of hash symbols defines the level of the header. The maximum supported level is six..
To define a line as a header, begin it with any number of consecutive hash symbols (#) and follow it with the title of your header. The number of hash symbols defines the level of the header. The maximum supported level is six.

Example:
```
Expand All @@ -113,19 +111,25 @@ Of course, you can also use basic formatting within the headers (e.g. `### Heade

### Links

Links follow the standard Markdown syntax of `[text to display](example.com)`. Additionally, you can add tooltips to your links with `[text to display](example.com "Some tooltip")`.
Links follow the standard Markdown syntax of `[text to display](https://example.com)`. Additionally, you can add tooltips to your links with `[text to display](https://example.com "Some tooltip")`.

"Autolinks" are also supported with their standard syntax: `<https://example.com>`, and `<mail@example.com>` for mail autolinks.

Links created this way will be automatically handled by MarkdownLabel, implemented their expected behaviour:

"Autolinks" are also supported with their standard syntax: `<example.com>`, and `<mail@example.com>` for mail autolinks.
- Valid header anchors (such as the ones in [Contents](#contents)) will make MarkdownLabel scroll to their header's position.
- Valid URLs and emails will be opened according to the user's settings (usually, using their default browser).
- Links that do not match any of the above conditions will be interpreted as a URL by prefixing them with "https://". E.g. `[link](example.com)` will link to "https://example.com".

Keep in mind that, in Godot, **links do nothing by default**. MarkdownLabel treats them the say way (may be changed in the future). See the [RichTextLabel reference](https://docs.godotengine.org/en/stable/tutorials/ui/bbcode_in_richtextlabel.html#doc-bbcode-in-richtextlabel-handling-url-tag-clicks) for more info.
This behavior can be disabled using the `automatic_links` property (enabled by default).

```
Markdown text .............................. -> BBCode equivalent
---------------------------------------------||------------------
[this is a link](example.com) .............. -> [url=example.com]this is a link[/url]
[this is a link](example.com "Example page") -> [hint=Example url][url=example.com]this is a link[/url][/hint]
<example.com> .............................. -> [url]example.com[/url]
<mail@example.com> ......................... -> [url=mailto:mail@example.com]mail@example.com[/url]
[this is a link](https://example.com) .............. -> [url=https://example.com]this is a link[/url]
[this is a link](https://example.com "Example page") -> [hint=Example url][url=https://example.com]this is a link[/url][/hint]
<https://example.com> .............................. -> [url]https://example.com[/url]
<mail@example.com> ................................. -> [url=mailto:mail@example.com]mail@example.com[/url]
```

### Images
Expand Down
89 changes: 67 additions & 22 deletions addons/markdownlabel/markdownlabel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ const _ESCAPE_PLACEHOLDER := ";$\uFFFD:%s$;"
const _ESCAPEABLE_CHARACTERS := "\\*_~`[]()\"<>#-+.!"
const _ESCAPEABLE_CHARACTERS_REGEX := "[\\\\\\*\\_\\~`\\[\\]\\(\\)\\\"\\<\\>#\\-\\+\\.\\!]"

# Public:
#region Public:
## The text to be displayed in Markdown format.
@export_multiline var markdown_text: String : set = _set_markdown_text

## If enabled, links will be automatically handled by this node, without needing to manually connect them. Valid header anchors will make the label scroll to that header's position. Valid URLs and e-mails will be opened according to the user's default settings.
@export var automatic_links := true

@export_group("Header formats")
## Formatting options for level-1 headers
@export var h1 := H1Format.new() : set = _set_h1_format
Expand All @@ -37,20 +40,27 @@ const _ESCAPEABLE_CHARACTERS_REGEX := "[\\\\\\*\\_\\~`\\[\\]\\(\\)\\\"\\<\\>#\\-
@export var h5 := H5Format.new() : set = _set_h5_format
## Formatting options for level-6 headers
@export var h6 := H6Format.new() : set = _set_h6_format
#endregion

# Private:
#region Private:
var _converted_text: String
var _indent_level: int
var _escaped_characters_map := {}
var _current_paragraph: int = 0
var _header_anchor_paragraph := {}
var _header_anchor_count := {}
var _within_table := false
var _table_row := -1
var _line_break := true
var _debug_mode := false
#endregion

# Built-in methods:
#region Built-in methods:
func _init(markdown_text: String = "") -> void:
bbcode_enabled = true
self.markdown_text = markdown_text
if automatic_links:
meta_clicked.connect(_on_meta_clicked)

func _ready() -> void:
h1.connect("_updated",_update)
Expand All @@ -70,18 +80,39 @@ func _ready() -> void:
#else:
#pass

# Should hide properties in the editor, not working for some reason:
#func _validate_property(property: Dictionary):
# print(property.name)
# if property.name in ["bbcode_enabled", "text"]:
# property.usage = PROPERTY_USAGE_NO_EDITOR
func _on_meta_clicked(meta: Variant) -> void:
if not automatic_links:
return
if typeof(meta) != TYPE_STRING:
return
if meta.begins_with("#") and meta in _header_anchor_paragraph:
self.scroll_to_paragraph(_header_anchor_paragraph[meta])
return
var url_pattern := RegEx.new()
url_pattern.compile("^(ftp|http|https):\\/\\/[^\\s\\\"]+$")
var result := url_pattern.search(meta)
if not result:
url_pattern.compile("^mailto:[^\\s]+@[^\\s]+\\.[^\\s]+$")
result = url_pattern.search(meta)
if result:
OS.shell_open(meta)
return
OS.shell_open("https://" + meta)

func _validate_property(property: Dictionary):
# Hide these properties in the editor:
if property.name in ["bbcode_enabled", "text"]:
property.usage = PROPERTY_USAGE_NO_EDITOR

#endregion

# Public methods:
#region Public methods:
## Reads the specified file and displays it as markdown.
func display_file(file_path: String):
func display_file(file_path: String) -> void:
markdown_text = FileAccess.get_file_as_string(file_path)
#endregion

#Private methods:
#region Private methods:
func _update() -> void:
text = _convert_markdown(markdown_text)
queue_redraw()
Expand Down Expand Up @@ -136,16 +167,18 @@ func _convert_markdown(source_text = "") -> String:

for line in lines:
line = line.trim_suffix("\r")
_debug("Parsing line: '%s'"%line)
_debug("Parsing line: '%s'" % line)
within_code_block = within_tilde_block or within_backtick_block
if iline > 0 and _line_break:
_converted_text += "\n"
_current_paragraph += 1
_line_break = true
iline+=1
if not within_tilde_block and _denotes_fenced_code_block(line,"`"):
if within_backtick_block:
if line.strip_edges().length() >= current_code_block_char_count:
_converted_text = _converted_text.trim_suffix("\n")
_current_paragraph -= 1
_converted_text += "[/code]"
within_backtick_block = false
_debug("... closing backtick block")
Expand All @@ -160,6 +193,7 @@ func _convert_markdown(source_text = "") -> String:
if within_tilde_block:
if line.strip_edges().length() >= current_code_block_char_count:
_converted_text = _converted_text.trim_suffix("\n")
_current_paragraph -= 1
_converted_text += "[/code]"
within_tilde_block = false
_debug("... closing tilde block")
Expand Down Expand Up @@ -365,13 +399,13 @@ func _convert_markdown(source_text = "") -> String:
break
n_spaces+=1
var header_format: Resource = _get_header_format(n)
var n_digits := str(header_format.font_size).length()
var _start := result.get_start()
var opening_tags := _get_header_tags(header_format)
_processed_line = _processed_line.erase(_start,n+n_spaces).insert(_start,opening_tags)
var _end := result.get_end()
_processed_line = _processed_line.insert(_end-(n+n_spaces)+opening_tags.length(),_get_header_tags(header_format,true))
_debug("... header level %d"%n)
_header_anchor_paragraph[_get_header_reference(result.get_string())] = _current_paragraph
else:
break

Expand All @@ -382,8 +416,8 @@ func _convert_markdown(source_text = "") -> String:
# end for line loop
# Close any remaining open list:
_debug("... end of text, closing all opened lists")
for i in range(_indent_level,-1,-1):
_converted_text += "[/%s]"%indent_types[i]
for i in range(_indent_level, -1, -1):
_converted_text += "[/%s]" % indent_types[i]
# Close any remaining open tables:
_debug("... end of text, closing all opened tables")
if _within_table:
Expand All @@ -398,16 +432,16 @@ func _convert_markdown(source_text = "") -> String:
func _process_list_syntax(line: String, indent_spaces: Array, indent_types: Array) -> String:
var processed_line := ""
if line.length() == 0 and _indent_level >= 0:
for i in range(_indent_level,-1,-1):
for i in range(_indent_level, -1, -1):
_converted_text += "[/%s]" % indent_types[_indent_level]
_indent_level-=1
_indent_level -= 1
indent_spaces.pop_back()
indent_types.pop_back()
_converted_text += "\n"
_debug("... empty line, closing all list tags")
return ""
if _indent_level == -1:
if line.length() > 2 and line[0] in "-*+" and line[1]==" ":
if line.length() > 2 and line[0] in "-*+" and line[1] == " ":
_indent_level = 0
indent_spaces.append(0)
indent_types.append("ul")
Expand Down Expand Up @@ -444,17 +478,17 @@ func _process_list_syntax(line: String, indent_spaces: Array, indent_types: Arra
_debug("... opening list at level %d and adding element"%_indent_level)
break
else:
for i in range(_indent_level,-1,-1):
for i in range(_indent_level, -1, -1):
if n_s < indent_spaces[i]:
_converted_text += "[/%s]"%indent_types[_indent_level]
_converted_text += "[/%s]" % indent_types[_indent_level]
_indent_level -= 1
indent_spaces.pop_back()
indent_types.pop_back()
else:
break
_converted_text += "\n"
processed_line = line.substr(n_s+2)
_debug("...closing lists down to level %d and adding element"%_indent_level)
_debug("...closing lists down to level %d and adding element" % _indent_level)
break
elif _char in "123456789":
if line.length() > n_s+3 and line[n_s+1] == "." and line[n_s+2] == " ":
Expand All @@ -481,7 +515,7 @@ func _process_list_syntax(line: String, indent_spaces: Array, indent_types: Arra
break
_converted_text += "\n"
processed_line = line.substr(n_s+3)
_debug("...closing lists down to level %d and adding element"%_indent_level)
_debug("... closing lists down to level %d and adding element"%_indent_level)
break
#end for _char loop
if processed_line.is_empty():
Expand Down Expand Up @@ -597,3 +631,14 @@ func _get_header_tags(header_format: Resource, closing := false) -> String:
if header_format.is_underlined:
tags += "[u]"
return tags

func _get_header_reference(header_string: String):
var anchor := "#" + header_string.lstrip("#").strip_edges().to_lower().replace(" ","-")
if anchor in _header_anchor_count:
_header_anchor_count[anchor] += 1
anchor += "-" + str(_header_anchor_count[anchor]-1)
else:
_header_anchor_count[anchor] = 1
return anchor

#endregion
2 changes: 1 addition & 1 deletion addons/markdownlabel/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="MarkdownLabel"
description="A custom node that extends RichTextLabel to use Markdown instead of BBCode."
author="Daenvil"
version="0.9.0"
version="1.1.0"
script="plugin.gd"
4 changes: 2 additions & 2 deletions tests/test_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
"title": "Escaping asterisks and underscores"
},
{
"input": "\\\~~foo~~\n\\\~\~\~\nbar\n\~\~\\\~",
"output": "\~\~foo\~\~\n\~\~\~\nbar\n\~\~\~",
"input": "\\~~foo~~\n\\~~~\nbar\n~~\\~",
"output": "~~foo~~\n~~~\nbar\n~~~",
"title": "Escaping tildes"
},
{
Expand Down

0 comments on commit 1e36d5b

Please sign in to comment.