Compare commits

...

38 Commits

Author SHA1 Message Date
c0759f0cbe add vscode folder to gitignore 2022-08-23 11:47:34 +03:00
47dc68028c Merge pull request 'make the endpoints at request time' (#9) from serve-on-request into main
Reviewed-on: #9
2022-07-04 20:04:06 +00:00
e44cac165a sync http-server with upstream and properly check for endpoints 2022-07-04 06:44:00 +03:00
3d2fd37191 make the endpoints at request time 2022-07-03 23:37:47 +03:00
836c101e6f properly update and use server_dir from ServerUI 2022-07-02 13:41:15 +03:00
8cbfffba94 Merge pull request 'add proper image handling to http server' (#7) from utf8-test into main
Reviewed-on: #7
2022-07-02 10:31:17 +00:00
7280277029 add proper image handling to http server 2022-07-02 02:11:29 +03:00
a203972157 update readme (#2) 2022-06-29 17:54:45 +03:00
7f7018f183 hide content preview container for now 2022-06-29 17:42:25 +03:00
44cfe464c5 change project title (#2) 2022-06-29 17:38:02 +03:00
df2dc1097e clean current file and tree item references when regenerating tree 2022-06-29 17:27:16 +03:00
68b4a03a3f implement refresh button 2022-06-29 16:53:46 +03:00
815aae5a4d implement open in browser button 2022-06-29 16:22:53 +03:00
6a98956b03 update to godot 3.5rc5 2022-06-29 16:15:46 +03:00
0602345af9 remove warnings in ServerUI 2022-06-29 16:14:14 +03:00
c8d19197e6 add newline to files.txt after stripping right edge 2022-06-29 14:02:37 +03:00
45cf485d9d disable file tree context menu while server is running 2022-06-29 13:55:44 +03:00
741e23af56 strip right edge when making files.txt 2022-06-29 13:51:46 +03:00
2284a1d1dd remove conditional when disabling nodes 2022-06-29 13:44:52 +03:00
d309c40fc3 disable file tree editing while server is running 2022-06-29 13:37:03 +03:00
fc1ca1e1b0 format MimeTypeHelper 2022-06-29 13:36:41 +03:00
12090f7930 improve file reading in ServerUI, remove dirty flag from FileDef 2022-06-25 20:29:18 +03:00
6dd063382c fixes #4 2022-06-25 20:21:50 +03:00
550a1c80a4 add server start functionality to ServerUI 2022-06-25 19:37:09 +03:00
80bc3e4a11 Merge pull request 'mimetypes' (#1) from mimetypes into main
Reviewed-on: yagich/ticle-godot-frontend#1
2022-06-25 14:45:50 +00:00
791743b765 Merge branch 'main' into mimetypes 2022-06-25 14:40:49 +00:00
e337f78740 remove redundant get_mime_type() function 2022-06-25 17:37:26 +03:00
514ba20c41 update Main scene file to instance the new ServerUI scene 2022-06-25 12:20:27 +03:00
003eb04013 improvement: use mimetype class & get rid of warnings 2022-06-25 10:56:45 +02:00
5380b59ee2 improvement: add a mimetype class 2022-06-25 10:56:00 +02:00
eeb6744947 add optional autosave to file content 2022-06-23 21:58:18 +03:00
d3764d3c4a add files.txt generation function 2022-06-23 21:35:07 +03:00
9fae276011 make sure tree items can't be moved out of bounds 2022-06-23 19:52:32 +03:00
c3e0bd043f add file reordering context menu 2022-06-23 19:52:32 +03:00
a6266ef98b add UI script, forgot to commit woopsie 2022-06-23 19:52:32 +03:00
e7d309efae add godot version disclaimer 2022-06-23 19:52:32 +03:00
9ba719e8d3 add initial UI 2022-06-21 18:23:15 +03:00
fe8aa24ca8 update ticle 2022-06-21 17:07:23 +03:00
13 changed files with 9679 additions and 461 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ export_presets.cfg
# Mono-specific ignores # Mono-specific ignores
.mono/ .mono/
data_*/ data_*/
# VSCode config folder
.vscode/

86
Main.gd
View File

@ -1,23 +1,15 @@
extends Control extends Control
const mime_types: Dictionary = { var mime_types := MimeTypeHelper.generate_db()
"html": "text/html",
"htm": "text/html",
"md": "text/plain",
"css": "text/css",
"txt": "text/plain",
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
}
var _server: HTTPServer = null var _server: HTTPServer = null
var files: Array = [] onready var server_ui := $ServerUI
onready var server_dir = server_ui.server_dir
func _ready() -> void: func _ready() -> void:
_start_server() pass
func _start_server(port: int = 3001) -> void: func _start_server(port: int = 3001) -> void:
@ -25,57 +17,61 @@ func _start_server(port: int = 3001) -> void:
return return
_server = HTTPServer.new() _server = HTTPServer.new()
var dir := Directory.new() _server.endpoint(HTTPServer.Method.GET, "/", funcref(self, "_serve_file"))
if dir.open("res://server_files") == OK:
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
if !dir.current_is_dir():
if file_name.get_extension() == "import":
file_name = dir.get_next()
continue
print(file_name) if _server.listen(port) != OK:
# TODO: show error to user here
return
_server.endpoint(HTTPServer.Method.GET, "/%s" % file_name, funcref(self, "_serve_file"), [file_name]) server_ui.is_server_running = true
file_name = dir.get_next()
_server.endpoint(HTTPServer.Method.GET, "/", funcref(self, "_serve_file"), ["index.html"])
_server.listen(port)
func _stop_server() -> void: func _stop_server() -> void:
if _server: if _server:
_server.stop()
_server = null _server = null
server_ui.is_server_running = false
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
if _server == null: if _server == null:
return return
_server.take_connection() if not _server.take_connection():
# TODO: show error to user here
return
func _serve_file(request: HTTPServer.Request, response: HTTPServer.Response, binds: Array) -> void: func _serve_file(request: HTTPServer.Request, response: HTTPServer.Response) -> void:
var file_name: String = binds[0] as String var file_name: String = request.endpoint()
print(file_name) if file_name == "/": # if the request is for root, serve index
file_name = "index.html"
var f := File.new()
var success = f.open(server_dir.plus_file(file_name), File.READ)
var f = File.new() if success == OK: # TODO: handle other errors like file not found
f.open("res://server_files/%s" % file_name, File.READ) var mime := mime_types.get(file_name)
response.type(mime)
var mime = get_mime_type(file_name) var data = f.get_buffer(f.get_len())
response.header("content-type", get_mime_type(file_name)) response.data(data)
response.data(f.get_as_text()) else:
# else: response.type(mime_types.get("txt"))
# response.header("content-type", "text/plain") response.status(500)
# response.data("500 - Read Error") response.data("Internal Server Error")
func get_mime_type(file_name: String) -> String: func _on_ServerUI_start_server_button_pressed(port: int, new_dir: String) -> void:
var ext := file_name.get_extension().to_lower() server_dir = new_dir
return mime_types[ext] if ext in mime_types else "application/octet-stream" _start_server(port)
func _on_ServerUI_stop_server_button_pressed() -> void:
_stop_server()
func _on_ServerUI_server_folder_changed(new_path: String) -> void:
server_dir = new_path

View File

@ -1,8 +1,15 @@
[gd_scene load_steps=2 format=2] [gd_scene load_steps=3 format=2]
[ext_resource path="res://Main.gd" type="Script" id=1] [ext_resource path="res://Main.gd" type="Script" id=1]
[ext_resource path="res://ServerUI.tscn" type="PackedScene" id=2]
[node name="Main" type="Control"] [node name="Main" type="Control"]
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
script = ExtResource( 1 ) script = ExtResource( 1 )
[node name="ServerUI" parent="." instance=ExtResource( 2 )]
[connection signal="server_folder_changed" from="ServerUI" to="." method="_on_ServerUI_server_folder_changed"]
[connection signal="start_server_button_pressed" from="ServerUI" to="." method="_on_ServerUI_start_server_button_pressed"]
[connection signal="stop_server_button_pressed" from="ServerUI" to="." method="_on_ServerUI_stop_server_button_pressed"]

View File

@ -1,7 +1,9 @@
# Godot frontend for Ticle # Godot frontend for Tickle
Ticle is a WIP tiny static site "generator" that parses Markdown files right in the browser, with no compilation to HTML necessary, intended for equally tiny blogs. Tickle is a WIP tiny static site "generator" that parses Markdown files right in the browser, with no compilation to HTML necessary, intended for equally tiny blogs.
This project aims to provide a nice frontend/UI to manage Ticle files and an easy way to run your site locally. This project aims to provide a nice frontend/UI to manage Tickle files and an easy way to run your site locally.
It uses a slightly modified version of [godot-http-server](https://github.com/velopman/godot-http-server) for the server side. It uses a slightly modified version of [godot-http-server](https://github.com/velopman/godot-http-server) for the server side.
The version of Godot used for this project is 3.5rc5.

359
ServerUI.gd Normal file
View File

@ -0,0 +1,359 @@
extends Control
const SERVER_STATUS_TEXT: Dictionary = {
true: "Server is running!",
false: "Server is not running.",
}
const START_BUTTON_SERVER_TEXT: Dictionary = {
true: "Stop server",
false: "Start server",
}
const ACCEPTED_FILE_FORMATS := ["md"]
export(bool) var enable_file_autosave := true # if true, will save the active file's contents every save_file_timeout seconds.
export(float) var save_file_timeout := 4.0 # the time to save document content after the edited signal of TextEdit
enum ContextMenuOptions {
MOVE_UP,
MOVE_DOWN,
}
onready var server_path_label := $"%ServerPathLabel"
onready var port_spin_box := $"%PortSpinBox"
onready var start_server_button := $"%StartServerButton"
onready var server_status_label := $"%ServerStatusLabel"
onready var open_browser_button := $"%OpenBrowserButton"
onready var document_title_lineedit := $"%DocTitleLineEdit"
onready var document_date_lineedit := $"%DocDateLineEdit"
onready var file_tree := $"%FileTree"
onready var file_tree_context_menu := $"%FileTreeContextMenu"
onready var document_input_textedit := $"%DocInputTextEdit"
onready var content_preview_richtextlabel := $"%ContentPreviewRichTextLabel"
onready var server_folder_dialog := $"%ServerFolderDialog"
signal server_folder_changed(new_path) # new_path: String
signal start_server_button_pressed(port, path) # port: int, path: String
signal stop_server_button_pressed() # emit from %StartServerButton, when the server is not running.
#signal server_port_changed(new_port) # new_port: int
#signal open_browser_button_pressed(port) # port: int
#signal files_selection_changed(new_files) # new_files: Array<String>
var server_dir: String setget set_server_dir
var is_server_running: bool = false setget set_server_running
var working_files: Array = [] # Array<FileDef>
var current_file: FileDef = null
var current_tree_selection: TreeItem = null
func _ready() -> void:
# TODO: put this in a config file
if !OS.has_feature("editor"):
self.server_dir = OS.get_executable_path().get_base_dir()
else:
self.server_dir = ProjectSettings.globalize_path("res://")
func set_server_dir(dir: String) -> void:
server_dir = dir
server_path_label.text = dir
file_tree.clear()
working_files.clear()
var root: TreeItem = file_tree.create_item()
root.set_text(0, "Server files")
var directory := Directory.new()
if directory.open(dir) == OK:
if directory.list_dir_begin() != OK:
push_error("Directory error") # TODO: show a user-facing error
var file_name: String = directory.get_next()
while file_name != "":
if !directory.current_is_dir() && (file_name.get_extension() in ACCEPTED_FILE_FORMATS):
var fd = FileDef(file_name, false)
working_files.append(fd)
fd.content = _get_file_content(file_name)
var in_filestxt := _is_file_in_filestxt(file_name)
fd.include_in_filestxt = in_filestxt
if in_filestxt:
var file_metadata := _get_file_metadata(file_name)
fd.title = file_metadata["title"]
fd.date = file_metadata["date"]
file_name = directory.get_next()
directory.list_dir_end()
_reconstruct_tree_from_working_files()
emit_signal("server_folder_changed", server_dir)
func _reconstruct_tree_from_working_files() -> void:
current_file = null
current_tree_selection = null
file_tree.clear()
var root: TreeItem = file_tree.create_item()
root.set_text(0, "Server files")
for i in working_files.size():
var fd = working_files[i] as FileDef
var item: TreeItem = file_tree.create_item(root)
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
item.set_metadata(0, {"file_def": fd, "id": i})
item.set_editable(0, true)
item.set_text(0, fd.file_path)
item.set_checked(0, fd.include_in_filestxt)
func _on_FileTree_item_edited() -> void:
var item: TreeItem = file_tree.get_edited()
var fd: FileDef = item.get_metadata(0)["file_def"]
fd.include_in_filestxt = item.is_checked(0)
func _on_FileTree_item_selected() -> void:
var item: TreeItem = file_tree.get_selected()
current_tree_selection = item
current_file = item.get_metadata(0)["file_def"] as FileDef
document_input_textedit.text = current_file.content
document_date_lineedit.text = current_file.date
document_title_lineedit.text = current_file.title
func _on_FileTree_item_rmb_selected(position: Vector2) -> void:
current_tree_selection = file_tree.get_selected()
file_tree_context_menu.rect_position = position + file_tree.rect_global_position
file_tree_context_menu.popup()
if is_server_running:
return # the file tree can't be edited while the server is running, so disable moving items as well
if (current_tree_selection.get_metadata(0)["id"] as int) == 0:
file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_UP, true)
else:
file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_UP, false)
if (current_tree_selection.get_metadata(0)["id"] as int) == working_files.size() - 1:
file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_DOWN, true)
else:
file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_DOWN, false)
func _on_OpenServerFolderButton_pressed() -> void:
server_folder_dialog.popup()
func set_server_running(running: bool) -> void:
is_server_running = running
port_spin_box.editable = !running
document_date_lineedit.editable = !running
document_title_lineedit.editable = !running
document_input_textedit.readonly = running
open_browser_button.disabled = !running
_set_file_tree_disabled(running)
_set_context_menu_disabled(running)
server_status_label.text = SERVER_STATUS_TEXT[running]
start_server_button.text = START_BUTTON_SERVER_TEXT[running]
func _generate_filestxt():
var files := ""
for file in working_files:
file = file as FileDef
if file.include_in_filestxt:
files += ("%s %s %s" % [file.file_path, file.date, file.title]).strip_edges(false, true) + "\n"
var f := File.new()
if f.open(server_dir.plus_file("files.txt"), File.WRITE) == OK:
f.store_string(files)
f.close()
else:
push_error("File.txt open for save error!") # TODO: show a user-facing error
func _on_StartServerButton_pressed() -> void:
if is_server_running:
emit_signal("stop_server_button_pressed")
else:
_generate_filestxt()
emit_signal("start_server_button_pressed", port_spin_box.value, server_dir)
func _is_file_in_filestxt(path: String) -> bool:
var f := File.new()
if f.open(server_dir.plus_file("files.txt"), File.READ) == OK:
var text := f.get_as_text()
return path in text
else:
return false
func _get_file_metadata(path: String) -> Dictionary:
var f := File.new()
var res = {
"file_path": path,
"date": "",
"title": "",
"content": "",
}
if f.open(server_dir.plus_file("files.txt"), File.READ) == OK:
var text := f.get_as_text()
var lines := text.split("\n")
for line in lines:
line = line as String
if line.empty():
continue
var def = line.split(" ", true, 2)
if def[0] == path:
# res["file_path"] = def[0]
res["date"] = def[1] if def.size() > 1 else ""
res["title"] = def[2] if def.size() > 2 else ""
break
res["content"] = _get_file_content(path)
return res
func _get_file_content(path: String) -> String:
var content: String = ""
var f := File.new()
if f.open(server_dir.plus_file(path), File.READ) == OK:
content = f.get_as_text()
return content
func _set_file_tree_disabled(disabled: bool) -> void:
var root = file_tree.get_root()
var tree_item = root.get_children() as TreeItem
while tree_item != null:
tree_item.set_editable(0, !disabled)
tree_item = tree_item.get_next()
func _set_context_menu_disabled(disabled: bool) -> void:
for i in file_tree_context_menu.get_item_count():
file_tree_context_menu.set_item_disabled(i, disabled)
func _commit_all_files() -> void:
for file in working_files:
file = file as FileDef
file.commit(server_dir)
func _on_FileTreeContextMenu_id_pressed(id: int) -> void:
var idx = current_tree_selection.get_metadata(0)["id"] as int
var fd = working_files[idx] as FileDef
match id:
ContextMenuOptions.MOVE_DOWN:
working_files.remove(idx)
working_files.insert(idx + 1, fd)
_reconstruct_tree_from_working_files()
ContextMenuOptions.MOVE_UP:
working_files.remove(idx)
working_files.insert(idx - 1, fd)
_reconstruct_tree_from_working_files()
func _on_DocTitleLineEdit_text_changed(new_text: String) -> void:
if current_file:
current_file.title = new_text
func _on_DocDateLineEdit_text_changed(new_text: String) -> void:
if current_file:
current_file.date = new_text
func _on_OpenBrowserButton_pressed() -> void:
if OS.shell_open("http://localhost:%s" % port_spin_box.value) != OK:
push_error("Error opening browser!") # TODO: show a user-facing error
func _on_RefreshFilesButton_pressed() -> void:
_commit_all_files()
_generate_filestxt()
self.server_dir = server_dir
func _on_DocInputTextEdit_text_changed() -> void:
if current_file:
var new_text: String = document_input_textedit.text
current_file.content = new_text
if !current_file.timer:
var t := Timer.new()
t.wait_time = save_file_timeout
t.one_shot = true
current_file.timer = t
add_child(t)
# warning-ignore:return_value_discarded
t.connect("timeout", self, "_on_EditedTimeout_timeout", [t, current_file])
else:
current_file.timer.stop()
current_file.timer.start()
func _on_EditedTimeout_timeout(timer: Timer, file: FileDef) -> void:
file.timer = null
timer.queue_free()
file.commit(server_dir)
func FileDef(
file_path: String,
include_in_filestxt: bool,
title: String = "",
date: String = "") -> FileDef:
var fd := FileDef.new()
fd.file_path = file_path
fd.include_in_filestxt = include_in_filestxt
fd.title = title
fd.date = date
return fd
class FileDef:
var file_path: String # relative
var include_in_filestxt: bool = true
var title: String # optional
var date: String # optional, YYYY-MM-DD
var content: String
var timer: Timer
func commit(server_dir: String) -> void:
var f := File.new()
var open := f.open(server_dir.plus_file(file_path), File.WRITE)
if open == OK:
f.store_string(content)
if timer && !timer.is_stopped(): # if there's an autosave pending, stop it since we're setting it here.
timer.stop()
else:
push_error("Error committing file %s, code %s" % [file_path, open]) # TODO: show a user-facing error

263
ServerUI.tscn Normal file
View File

@ -0,0 +1,263 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://ServerUI.gd" type="Script" id=1]
[node name="ServerUI" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_left = 0.0380859
anchor_top = 0.087
anchor_right = 0.961914
anchor_bottom = 0.936667
margin_top = -0.200005
custom_constants/separation = 9
__meta__ = {
"_edit_use_anchors_": true
}
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 1182.0
margin_bottom = 32.0
alignment = 1
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_left = 142.0
margin_top = 9.0
margin_right = 227.0
margin_bottom = 23.0
text = "Server folder:"
[node name="ServerPathLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 231.0
margin_top = 9.0
margin_right = 470.0
margin_bottom = 23.0
text = "/home/username/long/path/to/server"
[node name="OpenServerFolderButton" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 474.0
margin_right = 533.0
margin_bottom = 32.0
rect_min_size = Vector2( 0, 32 )
text = "Open..."
[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_left = 537.0
margin_top = 9.0
margin_right = 611.0
margin_bottom = 23.0
text = "Server port:"
[node name="PortSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 615.0
margin_right = 689.0
margin_bottom = 32.0
min_value = 81.0
max_value = 8000.0
value = 3001.0
[node name="StartServerButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 693.0
margin_right = 778.0
margin_bottom = 32.0
rect_min_size = Vector2( 0, 32 )
text = "Start server"
[node name="ServerStatusLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 782.0
margin_top = 9.0
margin_right = 918.0
margin_bottom = 23.0
text = "Server is not running."
[node name="OpenBrowserButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 922.0
margin_right = 1040.0
margin_bottom = 32.0
rect_min_size = Vector2( 0, 32 )
size_flags_horizontal = 12
disabled = true
text = "Open in browser"
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer"]
margin_top = 41.0
margin_right = 1182.0
margin_bottom = 611.0
size_flags_vertical = 3
split_offset = -263
[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"]
margin_right = 322.0
margin_bottom = 570.0
size_flags_horizontal = 3
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer2"]
margin_right = 322.0
margin_bottom = 32.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
margin_top = 9.0
margin_right = 30.0
margin_bottom = 23.0
size_flags_horizontal = 0
text = "Files"
[node name="RefreshFilesButton" type="Button" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
unique_name_in_owner = true
margin_left = 262.0
margin_right = 322.0
margin_bottom = 32.0
rect_min_size = Vector2( 0, 32 )
size_flags_horizontal = 10
text = "Refresh"
[node name="FileTree" type="Tree" parent="VBoxContainer/HSplitContainer/VBoxContainer2"]
unique_name_in_owner = true
margin_top = 36.0
margin_right = 322.0
margin_bottom = 570.0
size_flags_horizontal = 3
size_flags_vertical = 3
allow_rmb_select = true
hide_root = true
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"]
margin_left = 334.0
margin_right = 1182.0
margin_bottom = 570.0
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer"]
margin_right = 848.0
margin_bottom = 570.0
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"]
margin_right = 848.0
margin_bottom = 570.0
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_right = 848.0
margin_bottom = 24.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
margin_top = 5.0
margin_right = 32.0
margin_bottom = 19.0
text = "Title:"
[node name="DocTitleLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 36.0
margin_right = 848.0
margin_bottom = 24.0
size_flags_horizontal = 3
placeholder_text = "(Optional)"
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_top = 28.0
margin_right = 848.0
margin_bottom = 52.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
margin_top = 5.0
margin_right = 34.0
margin_bottom = 19.0
text = "Date:"
[node name="DocDateLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
margin_left = 38.0
margin_right = 848.0
margin_bottom = 24.0
size_flags_horizontal = 3
placeholder_text = "(Optional)"
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_top = 56.0
margin_right = 848.0
margin_bottom = 570.0
size_flags_vertical = 3
split_offset = 328
[node name="ContentEditContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer"]
margin_right = 848.0
margin_bottom = 514.0
size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
margin_right = 848.0
margin_bottom = 14.0
text = "Content"
[node name="DocInputTextEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
unique_name_in_owner = true
margin_top = 18.0
margin_right = 848.0
margin_bottom = 514.0
size_flags_horizontal = 3
size_flags_vertical = 3
show_line_numbers = true
wrap_enabled = true
[node name="ContentPreviewContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer"]
visible = false
margin_left = 390.0
margin_right = 848.0
margin_bottom = 514.0
size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentPreviewContainer"]
margin_right = 458.0
margin_bottom = 14.0
text = "Content Preview (Not accurate!)"
[node name="ContentPreviewRichTextLabel" type="RichTextLabel" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentPreviewContainer"]
unique_name_in_owner = true
margin_top = 18.0
margin_right = 458.0
margin_bottom = 514.0
size_flags_vertical = 3
[node name="ServerFolderDialog" type="FileDialog" parent="."]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -482.0
margin_top = -281.0
margin_right = 482.0
margin_bottom = 281.0
window_title = "Open a Directory"
mode = 2
access = 2
[node name="FileTreeContextMenu" type="PopupMenu" parent="."]
unique_name_in_owner = true
margin_right = 95.0
margin_bottom = 56.0
rect_min_size = Vector2( 95, 56 )
items = [ "Move Up", null, 0, false, false, 0, 0, null, "", false, "Move Down", null, 0, false, false, 1, 0, null, "", false ]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenServerFolderButton" to="." method="_on_OpenServerFolderButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/StartServerButton" to="." method="_on_StartServerButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenBrowserButton" to="." method="_on_OpenBrowserButton_pressed"]
[connection signal="pressed" from="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2/RefreshFilesButton" to="." method="_on_RefreshFilesButton_pressed"]
[connection signal="item_edited" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_edited"]
[connection signal="item_rmb_selected" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_rmb_selected"]
[connection signal="item_selected" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_selected"]
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer/DocTitleLineEdit" to="." method="_on_DocTitleLineEdit_text_changed"]
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2/DocDateLineEdit" to="." method="_on_DocDateLineEdit_text_changed"]
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer/DocInputTextEdit" to="." method="_on_DocInputTextEdit_text_changed"]
[connection signal="dir_selected" from="ServerFolderDialog" to="." method="set_server_dir"]
[connection signal="id_pressed" from="FileTreeContextMenu" to="." method="_on_FileTreeContextMenu_id_pressed"]

View File

@ -13,7 +13,7 @@ const Status = preload("res://addons/http_server/status.gd")
var __endpoints: Dictionary = { var __endpoints: Dictionary = {
# key: [Int, String], array with 0 index representing method, 1 index representing endpoint # key: [Int, String], array with 0 index representing method, 1 index representing endpoint
# value: [FuncRef, Array], index 0 = reference to function to call, index 1 = binds to pass to func # value: FuncRef, reference to function to call
} }
var __fallback: FuncRef = null var __fallback: FuncRef = null
var __server: TCP_Server = null var __server: TCP_Server = null
@ -32,7 +32,7 @@ func endpoint(type: int, endpoint: String, function: FuncRef, binds: Array = [])
) )
return return
__endpoints[endpoint_hash] = [function, binds] __endpoints[endpoint_hash] = function
func fallback(function: FuncRef) -> void: func fallback(function: FuncRef) -> void:
@ -106,7 +106,7 @@ func __process_connection(connection: StreamPeerTCP) -> void:
body = PoolStringArray(body_parts).join("\r\n") body = PoolStringArray(body_parts).join("\r\n")
var response: Response = __process_request(method, endpoint, headers, body) var response: Response = __process_request(method, endpoint, headers, body)
connection.put_data(response.to_utf8()) connection.put_data(response.get_data())
func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response: func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response:
@ -121,22 +121,15 @@ func __process_request(method: String, endpoint: String, headers: Dictionary, bo
var endpoint_func: FuncRef = null var endpoint_func: FuncRef = null
var endpoint_parts: PoolStringArray = endpoint.split("/", false) var endpoint_parts: PoolStringArray = endpoint.split("/", false)
var binds
# special case for if endpoint is just root while !endpoint_func:
if endpoint == "/": var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")]
var endpoint_hash: Array = [type, "/"]
if __endpoints.has(endpoint_hash): if __endpoints.has(endpoint_hash):
endpoint_func = __endpoints[endpoint_hash][0] endpoint_func = __endpoints[endpoint_hash]
binds = __endpoints[endpoint_hash][1] elif endpoint_parts.empty():
else: break
while (!endpoint_func && !endpoint_parts.empty()): else:
var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")] endpoint_parts.remove(endpoint_parts.size() - 1)
if __endpoints.has(endpoint_hash):
endpoint_func = __endpoints[endpoint_hash][0]
binds = __endpoints[endpoint_hash][1]
else:
endpoint_parts.remove(endpoint_parts.size() - 1)
if !endpoint_func: if !endpoint_func:
@ -160,10 +153,7 @@ func __process_request(method: String, endpoint: String, headers: Dictionary, bo
"[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint] "[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint]
) )
if !binds: endpoint_func.call_func(request, response)
endpoint_func.call_func(request, response)
else:
endpoint_func.call_func(request, response, binds)
return response return response

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ var __headers: Dictionary = {
# value: Variant, header value # value: Variant, header value
} }
var __status: int = 200 var __status: int = 200
var __type: MimeTypeHelper.MimeType
# Public methods # Public methods
@ -32,6 +33,10 @@ func status(status: int) -> void:
__status = status __status = status
func type(type: MimeTypeHelper.MimeType) -> void:
__type = type
func to_utf8() -> PoolByteArray: func to_utf8() -> PoolByteArray:
var content = PoolStringArray() var content = PoolStringArray()
@ -45,6 +50,7 @@ func to_utf8() -> PoolByteArray:
data = JSON.print(data) data = JSON.print(data)
__headers['content-length'] = len(data) __headers['content-length'] = len(data)
__headers["content-type"] = "application/octet-stream" if !__type else __type.full_type
for header in __headers: for header in __headers:
content.append("%s: %s" % [header, String(__headers[header])]) content.append("%s: %s" % [header, String(__headers[header])])
@ -55,3 +61,48 @@ func to_utf8() -> PoolByteArray:
content.append(data) content.append(data)
return content.join("\r\n").to_utf8() return content.join("\r\n").to_utf8()
func get_data() -> PoolByteArray:
var res = __response_headers()
var data = __data
if !data:
return res
var type: MimeTypeHelper.MimeType = __type
if !type:
type = MimeTypeHelper.MimeType.new()
if data is String: # else, assume data is PoolByteArray
data = data.to_utf8()
res.append_array(data)
return res
# Private methods
func __response_headers() -> PoolByteArray:
var res = PoolStringArray()
res.append(Status.code_to_status_line(__status))
var data = __data
if !data:
data = Status.code_to_description(__status)
__headers["content-length"] = len(data)
__headers["content-type"] = "application/octet-stream" if !__type else __type.full_type
for header in __headers:
res.append("%s: %s" % [header, String(__headers[header])])
res.append("")
var s = res.join("\r\n")
s = s + "\r\n"
return s.to_utf8()

View File

@ -13,17 +13,28 @@ _global_script_classes=[ {
"class": "HTTPServer", "class": "HTTPServer",
"language": "GDScript", "language": "GDScript",
"path": "res://addons/http_server/http_server.gd" "path": "res://addons/http_server/http_server.gd"
}, {
"base": "Reference",
"class": "MimeTypeHelper",
"language": "GDScript",
"path": "res://addons/http_server/mimetypes.gd"
} ] } ]
_global_script_class_icons={ _global_script_class_icons={
"HTTPServer": "" "HTTPServer": "",
"MimeTypeHelper": ""
} }
[application] [application]
config/name="Ticle Frontend" config/name="Tickle Frontend"
run/main_scene="res://Main.tscn" run/main_scene="res://Main.tscn"
config/icon="res://icon.png" config/icon="res://icon.png"
[display]
window/size/width=1280
window/size/height=720
[editor_plugins] [editor_plugins]
enabled=PoolStringArray( "res://addons/http_server/plugin.cfg" ) enabled=PoolStringArray( "res://addons/http_server/plugin.cfg" )

View File

@ -1,3 +1,2 @@
first.md 2022-01-06 first article first.md 2022-03-03 first title
second.md 2021-05-03 second article second.md 2022-03-05 second title
second.md

View File

@ -1,405 +1,248 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Parcel Sandbox</title> <title>Tickle</title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<style></style> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Lobster&family=Nunito+Sans:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet"
/>
<style>
:root {
--accent: hotpink;
--background: #fdfdfd;
font-family: "Nunito Sans", sans-serif;
}
body,
html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.trigger {
display: none;
}
.left-nav > * {
position: fixed;
top: 0;
left: 0;
}
.left-nav > .menu {
padding: 1em;
transform: translateX(-100%);
display: flex;
flex-direction: column;
top: 0;
bottom: 0;
transition: all 0.3s ease-out;
gap: 1em;
}
.left-nav > .menu a {
text-decoration: none;
background: var(--background);
color: var(--accent);
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
padding: 0.2em 0.4em;
}
.left-nav > .menu a:hover {
background: var(--accent);
color: var(--background);
}
.left-nav > .trigger:checked ~ .menu {
transform: translateX(0);
transition-duration: 0.1s;
}
.burger {
width: 2em;
height: 2em;
display: flex;
justify-content: center;
align-content: center;
text-align: center;
vertical-align: middle;
line-height: 2em;
border-radius: 0.2em;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
transition: background 0.5s ease;
}
.left-nav > .trigger:checked ~ .burger {
color: transparent;
}
.left-nav > .trigger:checked ~ .burger,
#Loading {
top: 0;
right: 0;
width: 100%;
height: 100%;
border-radius: 0;
background: rgba(17, 17, 17, 0.2);
}
#Loading {
position: absolute;
text-align: center;
align-items: center;
justify-content: center;
font-size: 8rem;
color: var(--accent);
opacity: 0;
transition: opacity 0.3s ease-in, height 0s linear 0.3s;
height: 0;
display: flex;
overflow: hidden;
}
.is-loading #Loading {
opacity: 1;
height: 100%;
transition: opacity 1s ease-in, height 0 linear;
}
#Loading::after {
content: "❤";
animation: beat 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes beat {
0% {
transform: scale(0.95);
}
5% {
transform: scale(1.1);
}
39% {
transform: scale(0.85);
}
45% {
transform: scale(1);
}
60% {
transform: scale(0.95);
}
100% {
transform: scale(0.9);
}
}
#Body {
padding: 3em;
max-width: 52em;
margin: 0 auto;
}
#Body h1,
#Body h2,
#Body h3,
#Body h4 {
color: var(--accent);
font-family: "Lobster", serif;
}
#Body a {
color: var(--background);
background-color: var(--accent);
text-decoration: none;
padding: 0.1em 0.4em;
}
</style>
</head> </head>
<body> <body>
<template id="my-link"> <nav class="left-nav">
<style> <input id="main-nav" type="checkbox" class="trigger" />
:host { <label for="main-nav" class="burger">&#8801;</label>
--background-regular: hsla(196, 61%, 58%, 0.75); <div id="Menu" class="menu"></div>
--background-active: red; </nav>
text-decoration: none; <header id="Menu"></header>
color: #18272f; <main id="Body"></main>
font-weight: 700; <div id="Loading"></div>
cursor: pointer; <script type="module">
position: relative; /**
display: flex; * markdown parser. Remove if you don't use markdown
} */
// @ts-ignore
:host span { import { micromark } from "https://esm.sh/micromark@3?bundle";
width: 100%;
height: 100%;
}
:host::before {
content: "";
background-color: var(--background-regular);
position: absolute;
left: 0;
bottom: 3px;
width: 100%;
height: 8px;
z-index: -1;
transition: all 0.3s ease-in-out;
}
:host(:hover)::before {
bottom: 0;
height: 100%;
}
:host([active])::before {
background-color: var(--background-active);
}
</style>
<span><slot /></span>
</template>
<template id="my-menu">
<style>
:host ul,
:host li {
list-style: none;
padding: 0;
margin: 0;
}
:host nav {
display: flex;
flex-direction: column;
}
</style>
<nav>
<slot />
</nav>
</template>
<my-menu id="menu">
<my-link main href="d">Home</my-link>
<h2>Articles</h2>
</my-menu>
<div id="App"></div>
<script>
//@ts-check
/**********************************************************************
*
* UTILITIES
*
* A few common methods to use in the project
*
*********************************************************************/
const Signal = () => {
const listeners = new Set();
return {
remove: listeners.delete.bind(listeners),
add(/** @type {(arg:any)=>void} */ listener) {
listeners.add(listener);
return listeners.delete.bind(listeners, listener);
},
emit(/** @type {any} */ data) {
listeners.forEach((l) => l(data));
},
};
};
const getText = (/** @type {string} */ file) =>
fetch(`./${file}`)
.then((response) => response.text())
.catch((err) => {
console.error(`could not find file "${file}"`);
throw err;
});
const parseMarkdown = (/** @type {string} */ text) =>
text
// lists
.replace(
/^\s*\n((?:\*\s.+\s*\n)+)([^\*])/gm,
(_, bullets, next) =>
`<ul>${bullets.replace(
/^\*\s(.+)/gm,
"<li>$1</li>"
)}\n</ul>\n\n${next}`
)
.replace(
/^\s*\n((?:\d\..+\s*\n)+)([^\*])/gm,
(_, bullets, next) =>
`<ol>${bullets.replace(
/^\d\.\s(.+)/gm,
"<li>$1</li>"
)}\n</ol>\n\n${next}`
)
// blockquotes
.replace(/^\>(.+)/gm, "<blockquote>$1</blockquote>")
// headers
.replace(/(#+)(.+)/g, (_, { length: l }, t) => `<h${l}>${t}</h${l}>`)
.replace(/^(.+)\n\=+/gm, "<h1>$1</h1>")
.replace(/^(.+)\n\-+/gm, "<h2>$1</h2>")
//images
.replace(/\!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1" />')
//links
.replace(
/[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g,
'<a href="$2" title="$4">$1</a>'
)
//font styles
.replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, "<strong>$1</strong>")
.replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, "<em>$1</em>")
.replace(/[\~]{2}([^\~]+)[\~]{2}/g, "<del>$1</del>")
//pre
.replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '<pre class="$2">')
.replace(/^\`\`\`\s*\n/gm, "</pre>\n\n")
//code
.replace(/[\`]{1}([^\`]+)[\`]{1}/g, "<code>$1</code>")
//p
.replace(/^\s*(\n)?(.+)/gm, (m) => {
return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m)
? m
: "<p>" + m + "</p>";
})
//strip p from pre
.replace(/(\<pre.+\>)\s*\n\<p\>(.+)\<\/p\>/gm, "$1$2")
.trim();
const getMarkdown = (/** @type {string} */ file) =>
getText(file).then(parseMarkdown);
/** /**
* useful to check for transitions while developing styles, if the loading screen disappears too fast
* *
* @param {string} tag
* @param {Record<string, any>} props
* @param {string|Node[]} children
* @returns
*/ */
const el = (tag = "div", props = {}, children = []) => { const wait = (val) => new Promise((ok) => setTimeout(ok, 1, val));
const node = document.createElement(tag);
Object.keys(props).forEach((key) => { /**
node.setAttribute(key, props[key]); * The elements we will need
}); */
if (typeof children == "string") { const [Menu, Body, Loading] = ["Menu", "Body", "Loading"].map((id) =>
children = [document.createTextNode(children)]; document.getElementById(id)
);
/**
* cache the original title to append it to the page title
*/
const mainTitle = document.title;
const startLoading = () => document.body.classList.add("is-loading");
const stopLoading = () => document.body.classList.remove("is-loading");
const getCurrentPage = () =>
window.location.hash[1] === "/" ? window.location.hash.slice(2) : "";
const onHashChange = (evt) => {
const path = getCurrentPage();
if (!path) {
return false;
} }
children.forEach((child) => node.appendChild(child)); startLoading();
return node; return fetch(`./${path}`)
}; .then((response) => response.text())
.then(wait)
const makeTitelize = (alwaysLowCaps = [], alwaysUpperCaps = []) => { .then((text) => {
const specials = [...alwaysLowCaps, ...alwaysUpperCaps].reduce( const [, title] = text.match(/^(#\s\w+)/) ||
(result, word) => text.match(/(.*?)\n===+/m) || [, path];
result.set(new RegExp("\\b" + word + "\\b", "gi"), word), document.title = `${title} | ${mainTitle}`;
/** @type {Map<RegExp, string>}*/ (new Map()) Body.innerHTML = micromark(text);
); stopLoading();
const titelize = (/** @type {string} */ text) => {
text = text
.replace(/_-\//g, " ")
.replace(/\.\w+$/, "")
.replace(/\s+/, " ")
.split(" ")
.map((word) =>
word.length > 1
? word[0].toUpperCase() + word.slice(1).toLowerCase()
: word
)
.join(" ");
for (const [key, value] of specials) {
text = text.replace(key, value);
}
return text;
};
return titelize;
};
const titelize = makeTitelize(["the", "a"], ["TV", "ID", "AI"]);
const Router = (() => {
const onRouteChange = Signal();
let route = "";
const set = (/** @type {string} */ newRoute) => {
if (newRoute === route) {
return false;
}
window.location.hash = newRoute;
route = newRoute;
onRouteChange.emit(route);
return true;
};
const get = () => window.location.hash.slice(1).replace(/\//gi, "/");
const is = (href) => href === get();
window.addEventListener("popstate", () => set(get()));
return { set, get, is, onRouteChange };
})();
const getTemplateClone = (/** @type {string} */ id) => {
const templateModel = /** @type {HTMLTemplateElement} */ (
document.getElementById(id)
);
const template = /** @type {HTMLElement} */ (
templateModel.content.cloneNode(true)
);
return template;
};
/**********************************************************************
* WEB COMPONENTS
*
* Sources:
* https://web.dev/custom-elements-best-practices/
* https://googlechromelabs.github.io/howto-components/
*
* A set of neat components to use in the page
*
*********************************************************************/
class CustomElement extends HTMLElement {
/** @type {ShadowRoot} */
shadow = this.attachShadow({ mode: "closed" });
/**
* A user may set a property on an instance of an element, before its prototype has been connected to this class.
* Will check for any instance properties and run them through the proper class setters.
* @param {string} prop
*/
_syncProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
}
class MyLink extends CustomElement {
constructor() {
super();
this.shadow.append(getTemplateClone("my-link"));
this.shadow.addEventListener("click", this._onClick.bind(this));
Router.onRouteChange.add(this.updateActive.bind(this));
}
static get observedAttributes() {
return ["href", "active", "main"];
}
_onClick() {
if (this.href) {
Router.set(this.href);
}
}
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
this[property] = newValue;
}
updateActive() {
if (Router.is(this.href)) {
this.setAttribute("active", "");
} else {
this.removeAttribute("active");
}
}
set href(/** @type {string}*/ value) {
this.setAttribute("href", value);
this.updateActive();
}
get href() {
return this.getAttribute("href");
}
set main(/** @type {boolean}*/ value) {
if (value) {
this.setAttribute("main", "");
} else {
this.removeAttribute("main");
}
}
get main() {
return this.hasAttribute("main");
}
connectedCallback() {
["active", "main"].forEach((prop) => this._syncProperty(prop));
this.updateActive();
if (this.getAttribute("main")) {
console.log("sdfsdff");
}
}
}
customElements.define("my-link", MyLink);
class MyMenu extends CustomElement {
_handled = new Set();
constructor() {
super();
this.shadow.append(getTemplateClone("my-menu"));
const slot = this.shadow.querySelector("slot");
slot.addEventListener("slotchange", (event) => {
for (const child of slot.assignedElements()) {
if (this._handled.has(child) || !(child instanceof MyLink)) {
continue;
}
this._handled.add(child);
// TODO: pre-fetch
//console.log("new child: ", child);
}
}); });
} };
}
customElements.define("my-menu", MyMenu);
/********************************************************************** window.addEventListener("hashchange", onHashChange);
* MARKDOWN PARSING
*********************************************************************/
const load = (/** @type {string} */ file) => /**
getMarkdown(file).then((md) => { * Loads the article list, parses it, creates the menu items
app.innerHTML = md; */
}); const start = () => {
startLoading();
fetch("./files.txt")
.then((response) => response.text())
.then((lines) => {
Menu.innerHTML = lines
.split(`\n`)
.map((line, index) => {
const today = new Date().toISOString().split("T")[0];
const result = line.match(
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s(?<date>[\d-]+)?(?<title>.+))?/
);
if (!result) {
console.log(`could not parse line ${index}: [${line}]`);
return null;
}
const {
groups: { name, ext, date = today, title = name },
} = result;
const href = `/${name}.${ext}`;
if (!getCurrentPage()) {
window.location.hash = `#${href}`;
}
return { name, href, date, title };
})
.filter(Boolean)
.sort(({ date: a }, { date: b }) => a - b)
.map(
({ href, title }) => `<a data-link href="#${href}">${title}</a>`
)
.join(`\n`);
/********************************************************************** onHashChange();
* BOOSTRAPPING
*********************************************************************/
getText("files.txt").then((lines) => {
lines
.split(`\n`)
.map((line) => {
const [file, maybeDate, ...rest] = line.split(/\s/);
const href = file.trim();
let date = maybeDate ? new Date(maybeDate) : new Date();
if (isNaN(date.getTime())) {
date = new Date();
rest.unshift(maybeDate);
}
const textContent = rest.length
? rest.join(" ").trim()
: titelize(file);
return { href, date, textContent };
})
.sort(({ date: a }, { date: b }) => a.getTime() - b.getTime())
.forEach(({ href, date, textContent }) => {
const link = /** @type {MyLink} */ el(
"my-link",
{ href },
textContent
);
document.getElementById("menu").appendChild(link);
}); });
}); };
const app = document.getElementById("App"); start();
const BLOCKQUOTE = Symbol("blockquote");
const PARAGRAPH = Symbol("paragraph");
const LIST = Symbol("list");
Router.onRouteChange.add((route) => load(route));
</script> </script>
</body> </body>
</html> </html>

View File

@ -17,4 +17,4 @@ Image:
code_block! code_block!
``` ```
`code not block` `code not block`