[#3] Split the tavern project into a workspace.
**tavern** - The blogging system library. **loremaster** - Creates the database from a blogging repository. **bard** - Dioxus components to display the blog.
This commit is contained in:
79
tavern/.rustfmt.toml
Normal file
79
tavern/.rustfmt.toml
Normal file
@ -0,0 +1,79 @@
|
||||
max_width = 80
|
||||
hard_tabs = false
|
||||
tab_spaces = 3
|
||||
newline_style = "Unix"
|
||||
indent_style = "Visual"
|
||||
use_small_heuristics = "Default"
|
||||
fn_call_width = 60
|
||||
attr_fn_like_width = 70
|
||||
struct_lit_width = 18
|
||||
struct_variant_width = 35
|
||||
array_width = 60
|
||||
chain_width = 60
|
||||
single_line_if_else_max_width = 50
|
||||
single_line_let_else_max_width = 50
|
||||
wrap_comments = true
|
||||
format_code_in_doc_comments = true
|
||||
doc_comment_code_block_width = 80
|
||||
comment_width = 80
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
format_strings = true
|
||||
format_macro_matchers = true
|
||||
format_macro_bodies = true
|
||||
skip_macro_invocations = []
|
||||
hex_literal_case = "Preserve"
|
||||
empty_item_single_line = true
|
||||
struct_lit_single_line = true
|
||||
fn_single_line = false
|
||||
where_single_line = true
|
||||
imports_indent = "Visual"
|
||||
imports_layout = "Horizontal"
|
||||
imports_granularity = "Module"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
reorder_impl_items = true
|
||||
type_punctuation_density = "Wide"
|
||||
space_before_colon = false
|
||||
space_after_colon = true
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Back"
|
||||
remove_nested_parens = true
|
||||
combine_control_expr = false
|
||||
short_array_element_width_threshold = 10
|
||||
overflow_delimited_expr = false
|
||||
struct_field_align_threshold = 0
|
||||
enum_discrim_align_threshold = 0
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
force_multiline_blocks = true
|
||||
fn_params_layout = "Compressed"
|
||||
brace_style = "AlwaysNextLine"
|
||||
control_brace_style = "AlwaysNextLine"
|
||||
trailing_semicolon = true
|
||||
trailing_comma = "Never"
|
||||
match_block_trailing_comma = false
|
||||
blank_lines_upper_bound = 3
|
||||
blank_lines_lower_bound = 0
|
||||
edition = "2021"
|
||||
style_edition = "2021"
|
||||
inline_attribute_width = 0
|
||||
format_generated_files = true
|
||||
generated_marker_line_search_limit = 5
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Always"
|
||||
required_version = "1.8.0"
|
||||
unstable_features = true
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
show_parse_errors = true
|
||||
error_on_line_overflow = false
|
||||
error_on_unformatted = false
|
||||
ignore = []
|
||||
emit_mode = "Files"
|
||||
make_backup = false
|
||||
12
tavern/.sqlx/query-0f58e58ff3bcda95267629c8dbde46f85218872afb759b07d5aa217309555b99.json
generated
Normal file
12
tavern/.sqlx/query-0f58e58ff3bcda95267629c8dbde46f85218872afb759b07d5aa217309555b99.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR IGNORE INTO tale_tags (tale_slug, tag_id)\n VALUES (?1, ?2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0f58e58ff3bcda95267629c8dbde46f85218872afb759b07d5aa217309555b99"
|
||||
}
|
||||
12
tavern/.sqlx/query-2b2ddeb7ea1690d809f237afc8d21aa67599591bcbc9671d31c6ed90aded502d.json
generated
Normal file
12
tavern/.sqlx/query-2b2ddeb7ea1690d809f237afc8d21aa67599591bcbc9671d31c6ed90aded502d.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR REPLACE INTO tavern (key, value) VALUES ('title', ?1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2b2ddeb7ea1690d809f237afc8d21aa67599591bcbc9671d31c6ed90aded502d"
|
||||
}
|
||||
12
tavern/.sqlx/query-40a4dcf62e6741b1e2b669469704325d06de1327c453945128ce2e0a1edf510d.json
generated
Normal file
12
tavern/.sqlx/query-40a4dcf62e6741b1e2b669469704325d06de1327c453945128ce2e0a1edf510d.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR REPLACE INTO adventurers (\n handle, name, profile, image, blurb\n ) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "40a4dcf62e6741b1e2b669469704325d06de1327c453945128ce2e0a1edf510d"
|
||||
}
|
||||
12
tavern/.sqlx/query-50d891dc85cb19ce33378ced606b10ac982c6fdcd30e6d089a1d140210c9b11a.json
generated
Normal file
12
tavern/.sqlx/query-50d891dc85cb19ce33378ced606b10ac982c6fdcd30e6d089a1d140210c9b11a.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "CREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL UNIQUE\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50d891dc85cb19ce33378ced606b10ac982c6fdcd30e6d089a1d140210c9b11a"
|
||||
}
|
||||
12
tavern/.sqlx/query-5bbbe0b55b0e8a775165a59c2344bf5cbd7028ca60047a869de25e7931920190.json
generated
Normal file
12
tavern/.sqlx/query-5bbbe0b55b0e8a775165a59c2344bf5cbd7028ca60047a869de25e7931920190.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR IGNORE INTO tags (name) VALUES (?1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5bbbe0b55b0e8a775165a59c2344bf5cbd7028ca60047a869de25e7931920190"
|
||||
}
|
||||
12
tavern/.sqlx/query-63586c7d68985fedfb450a942ad03b81fe2f78bb0b317c75d9af6266c6ad6d55.json
generated
Normal file
12
tavern/.sqlx/query-63586c7d68985fedfb450a942ad03b81fe2f78bb0b317c75d9af6266c6ad6d55.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "CREATE TABLE IF NOT EXISTS tales (\n slug TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n author TEXT NOT NULL,\n summary TEXT NOT NULL,\n publish_date TEXT NOT NULL,\n content TEXT NOT NULL\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "63586c7d68985fedfb450a942ad03b81fe2f78bb0b317c75d9af6266c6ad6d55"
|
||||
}
|
||||
12
tavern/.sqlx/query-67c82ddcbec08a947ef28a28a439bbf3838edfc2e9ce27e6a6661505e5b936e2.json
generated
Normal file
12
tavern/.sqlx/query-67c82ddcbec08a947ef28a28a439bbf3838edfc2e9ce27e6a6661505e5b936e2.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "CREATE TABLE IF NOT EXISTS adventurers (\n handle TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n profile TEXT NOT NULL,\n image TEXT NOT NULL,\n blurb TEXT NOT NULL\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "67c82ddcbec08a947ef28a28a439bbf3838edfc2e9ce27e6a6661505e5b936e2"
|
||||
}
|
||||
20
tavern/.sqlx/query-b1fa9c554e3fe18b4117a314c644cc5bf969e512b9fb6b589bd09504317363c0.json
generated
Normal file
20
tavern/.sqlx/query-b1fa9c554e3fe18b4117a314c644cc5bf969e512b9fb6b589bd09504317363c0.json
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id FROM tags WHERE name = ?1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b1fa9c554e3fe18b4117a314c644cc5bf969e512b9fb6b589bd09504317363c0"
|
||||
}
|
||||
12
tavern/.sqlx/query-c32d614137f871a3f603c57e99d62b293515e469653452750ed9e5424be00320.json
generated
Normal file
12
tavern/.sqlx/query-c32d614137f871a3f603c57e99d62b293515e469653452750ed9e5424be00320.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "CREATE TABLE IF NOT EXISTS tale_tags (\n tale_slug TEXT,\n tag_id INTEGER,\n FOREIGN KEY(tale_slug) REFERENCES tales(slug),\n FOREIGN KEY(tag_id) REFERENCES tags(id),\n UNIQUE(tale_slug, tag_id)\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c32d614137f871a3f603c57e99d62b293515e469653452750ed9e5424be00320"
|
||||
}
|
||||
12
tavern/.sqlx/query-e448c3365fa62303d143b2ed04ee4e230b99d780768c96de7966fbee252e7565.json
generated
Normal file
12
tavern/.sqlx/query-e448c3365fa62303d143b2ed04ee4e230b99d780768c96de7966fbee252e7565.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR REPLACE INTO tales (\n slug, title, author, summary, publish_date, content\n ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e448c3365fa62303d143b2ed04ee4e230b99d780768c96de7966fbee252e7565"
|
||||
}
|
||||
12
tavern/.sqlx/query-ec49fe1746763238c7ead570da9b7800e68e1e7311c16ea07d9e50904b40e817.json
generated
Normal file
12
tavern/.sqlx/query-ec49fe1746763238c7ead570da9b7800e68e1e7311c16ea07d9e50904b40e817.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "CREATE TABLE IF NOT EXISTS tavern (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ec49fe1746763238c7ead570da9b7800e68e1e7311c16ea07d9e50904b40e817"
|
||||
}
|
||||
12
tavern/.sqlx/query-ee6075930ca151fc036d2797b96b29c65de57982428e1a6f45579638b6c7442a.json
generated
Normal file
12
tavern/.sqlx/query-ee6075930ca151fc036d2797b96b29c65de57982428e1a6f45579638b6c7442a.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT OR REPLACE INTO tavern (key, value) VALUES ('description', ?1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ee6075930ca151fc036d2797b96b29c65de57982428e1a6f45579638b6c7442a"
|
||||
}
|
||||
2059
tavern/Cargo.lock
generated
Normal file
2059
tavern/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
tavern/Cargo.toml
Normal file
26
tavern/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "tavern"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "A blogging system that will allow you to write your blog in Markdown and then display it in HTML using Dioxus."
|
||||
repository = "/CyberMages/tavern"
|
||||
authors = ["CyberMages LLC <Software@CyberMagesLLC.com>", "Jason Travis Smith <Myrddin@CyberMages.tech>"]
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
pulldown-cmark = "0.13.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = { version = "0.8.6", features = ["sqlite", "chrono", "runtime-tokio"] }
|
||||
toml = "0.9.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
|
||||
|
||||
[features]
|
||||
publisher = []
|
||||
174
tavern/LICENSE.md
Normal file
174
tavern/LICENSE.md
Normal file
@ -0,0 +1,174 @@
|
||||
# Apache License
|
||||
Version 2.0, January 2004
|
||||
<http://www.apache.org/licenses/>
|
||||
|
||||
## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
### 1. Definitions.
|
||||
|
||||
**"License"** shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
**"Licensor"** shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
**"Legal Entity"** shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
**"You" (or "Your")** shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
**"Source" form** shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation source,
|
||||
and configuration files.
|
||||
|
||||
**"Object" form** shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
**"Work"** shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
**"Derivative Works"** shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
**"Contribution"** shall mean any work of authorship, including the
|
||||
original version of the Work and any modifications or additions to that
|
||||
Work or Derivative Works thereof, that is intentionally submitted to
|
||||
the Licensor for inclusion in the Work by the copyright owner or by an
|
||||
individual or Legal Entity authorized to submit on behalf of the
|
||||
copyright owner.
|
||||
|
||||
**"Contributor"** shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
### 2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable copyright license to reproduce, prepare
|
||||
Derivative Works of, publicly display, publicly perform, sublicense,
|
||||
and distribute the Work and such Derivative Works in Source or Object
|
||||
form.
|
||||
|
||||
### 3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor
|
||||
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent
|
||||
license to make, have made, use, offer to sell, sell, import, and
|
||||
otherwise transfer the Work, where such license applies only to those
|
||||
patent claims licensable by such Contributor that are necessarily
|
||||
infringed by their Contribution(s) alone or by combination of their
|
||||
Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
the Work or a Contribution incorporated within the Work constitutes
|
||||
direct or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate as of
|
||||
the date such litigation is filed.
|
||||
|
||||
### 4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works
|
||||
thereof in any medium, with or without modifications, and in Source or
|
||||
Object form, provided that You meet the following conditions:
|
||||
|
||||
1. You must give any other recipients of the Work or Derivative Works a
|
||||
copy of this License; and
|
||||
|
||||
2. You must cause any modified files to carry prominent notices stating
|
||||
that You changed the files; and
|
||||
|
||||
3. You must retain, in the Source form of any Derivative Works that You
|
||||
distribute, all copyright, patent, trademark, and attribution notices
|
||||
from the Source form of the Work, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works; and
|
||||
|
||||
4. If the Work includes a "NOTICE" text file as part of its distribution,
|
||||
then any Derivative Works that You distribute must include a readable
|
||||
copy of the attribution notices contained within such NOTICE file,
|
||||
excluding those notices that do not pertain to any part of the
|
||||
Derivative Works, in at least one of the following places: within a
|
||||
NOTICE text file distributed as part of the Derivative Works; within
|
||||
the Source form or documentation, if provided along with the Derivative
|
||||
Works; or, within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents of the
|
||||
NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative
|
||||
Works that You distribute, alongside or as an addendum to the NOTICE
|
||||
text from the Work, provided that such additional attribution notices
|
||||
cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may
|
||||
provide additional or different license terms and conditions for use,
|
||||
reproduction, or distribution of Your modifications, or for any such
|
||||
Derivative Works as a whole, provided Your use, reproduction, and
|
||||
distribution of the Work otherwise complies with the conditions stated
|
||||
in this License.
|
||||
|
||||
### 5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally
|
||||
submitted for inclusion in the Work by You to the Licensor shall be
|
||||
under the terms and conditions of this License, without any additional
|
||||
terms or conditions. Notwithstanding the above, nothing herein shall
|
||||
supersede or modify the terms of any separate license agreement you
|
||||
may have executed with Licensor regarding such Contributions.
|
||||
|
||||
### 6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names,
|
||||
trademarks, service marks, or product names of the Licensor, except as
|
||||
required for describing the origin of the Work and reproducing the
|
||||
content of the NOTICE file.
|
||||
|
||||
### 7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor
|
||||
provides the Work (and each Contributor provides its Contributions) on
|
||||
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
express or implied, including, without limitation, any warranties or
|
||||
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
### 8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including
|
||||
negligence), contract, or otherwise, unless required by applicable law
|
||||
(such as deliberate and grossly negligent acts) or agreed to in writing,
|
||||
shall any Contributor be liable to You for damages, including any direct,
|
||||
indirect, special, incidental, or consequential damages of any character
|
||||
arising as a result of this License or out of the use or inability to use
|
||||
the Work (including but not limited to damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, or any and all other commercial
|
||||
damages or losses), even if such Contributor has been advised of the
|
||||
possibility of such damages.
|
||||
|
||||
### 9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose
|
||||
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this License.
|
||||
However, in accepting such obligations, You may act only on Your own behalf
|
||||
and on Your sole responsibility, not on behalf of any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor
|
||||
harmless for any liability incurred by, or claims asserted against, such
|
||||
Contributor by reason of your accepting any such warranty or additional
|
||||
liability.
|
||||
19
tavern/README.md
Normal file
19
tavern/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Tavern
|
||||
|
||||
A blogging system that will allow you to write your blog in Markdown and then
|
||||
display it in HTML using Dioxus.
|
||||
|
||||
---
|
||||
|
||||
## Copyright & License
|
||||
|
||||
Copyright 2025 CyberMages LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this library except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS
|
||||
135
tavern/examples/generate_database.rs
Normal file
135
tavern/examples/generate_database.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use tavern::{Adventurer, Database, Legend, Lore, Tale, Tavern};
|
||||
|
||||
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
fn generate_tavern() -> Tavern
|
||||
{
|
||||
let legend: Legend = Legend
|
||||
{
|
||||
profile:
|
||||
String::from("https://cybermages.tech/about/myrddin"),
|
||||
image:
|
||||
String::from("https://cybermages.tech/about/myrddin/pic"),
|
||||
blurb: String::from("I love code!") };
|
||||
|
||||
let author: Adventurer =
|
||||
Adventurer { name: String::from("Jason Smith"),
|
||||
handle: String::from("myrddin"),
|
||||
legend };
|
||||
|
||||
let lore: Lore =
|
||||
Lore { title: String::from("Test post"),
|
||||
slug: String::from("test_post"),
|
||||
author: author.handle.clone(),
|
||||
summary: String::from("The Moon is made of cheese!"),
|
||||
tags: vec![String::from("Space"),
|
||||
String::from("Cheese")],
|
||||
publish_date:
|
||||
NaiveDate::from_ymd_opt(2025, 12, 25).unwrap()
|
||||
.and_hms_opt(13,
|
||||
10, 41)
|
||||
.unwrap() };
|
||||
|
||||
let tale: Tale = Tale { lore,
|
||||
story: PathBuf::from("posts/test_post.md") };
|
||||
|
||||
|
||||
// Create a dummy posts directory and file for this example to work
|
||||
if !Path::new("posts").exists()
|
||||
{
|
||||
std::fs::create_dir("posts").unwrap();
|
||||
}
|
||||
std::fs::write("posts/the-rustacean.md",
|
||||
"# Hello, Rust!\n\nThis is a **test** post.").unwrap();
|
||||
|
||||
|
||||
Tavern { title: String::from("Runes & Ramblings"),
|
||||
description: String::from("Join software engineer Jason Smith \
|
||||
on his Rust programming journey. \
|
||||
Explore program design, tech \
|
||||
stacks, and more on this blog from \
|
||||
CybeMages, LLC."),
|
||||
tales: vec![tale],
|
||||
authors: vec![author] }
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
fn read_from_file<P>(config_file: P) -> Tavern
|
||||
where P: AsRef<Path>
|
||||
{
|
||||
// Read the previously written TOML file
|
||||
let toml_data =
|
||||
std::fs::read_to_string(&config_file).expect("Failed to read TOML file");
|
||||
|
||||
// Deserialize it
|
||||
toml::from_str(&toml_data).expect("Failed to parse TOML")
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
async fn create_database() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
// This part would be the entry point of your CI/CD script
|
||||
// It would load your data and then save it to the database
|
||||
|
||||
// Create a Tavern object
|
||||
let _tavern = read_from_file("Tavern.toml");
|
||||
// let tavern = generate_tavern();
|
||||
|
||||
// Open the database and save the Tavern content
|
||||
let _db = Database::open("/home/myrddin/cybermages/blog/tavern.db").await?;
|
||||
//db.insert_tavern(&tavern.title, &tavern.description)?;
|
||||
//println!("Saved site settings: Title='{}', Description='{}'",
|
||||
//tavern.title, tavern.description);
|
||||
|
||||
//for author in &tavern.authors
|
||||
// {
|
||||
// db.insert_adventurer(author)?;
|
||||
// println!("Saved adventurer: {}", author.name);
|
||||
// }
|
||||
//
|
||||
// for tale in &tavern.tales
|
||||
// {
|
||||
// db.insert_tale(tale)?;
|
||||
// println!("Saved tale: {}", tale.title);
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
#[tokio::main]
|
||||
pub async fn main()
|
||||
{
|
||||
match std::env::set_current_dir("/home/myrddin/cybermages/blog/")
|
||||
{
|
||||
Ok(_) =>
|
||||
{
|
||||
println!("Successfully changed working directory.");
|
||||
}
|
||||
Err(e) =>
|
||||
{
|
||||
eprintln!("Failed to change directory: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match create_database().await
|
||||
{
|
||||
Ok(_) =>
|
||||
{}
|
||||
Err(e) =>
|
||||
{
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
pub fn main()
|
||||
{
|
||||
}
|
||||
40
tavern/src/adventurer.rs
Normal file
40
tavern/src/adventurer.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
///
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Legend
|
||||
{
|
||||
/// A link to the adventurer's profile (e.g., personal website, GitHub,
|
||||
/// etc.).
|
||||
pub profile: String,
|
||||
|
||||
/// A URL or path to an image representing the adventurer (e.g., avatar or
|
||||
/// portrait).
|
||||
pub image: String,
|
||||
|
||||
/// A short descriptive text or tagline about the adventurer.
|
||||
pub blurb: String
|
||||
}
|
||||
|
||||
/// Represents an author or contributor of a tale.
|
||||
///
|
||||
/// An `Adventurer` contains identifying and descriptive information
|
||||
/// such as their name, handle, profile URL, image, and a short blurb.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Adventurer
|
||||
{
|
||||
/// The full name of the adventurer.
|
||||
pub name: String,
|
||||
|
||||
/// A unique handle or username for the adventurer (e.g., used in URLs or
|
||||
/// mentions).
|
||||
pub handle: String,
|
||||
|
||||
///
|
||||
#[serde(flatten)]
|
||||
pub legend: Legend
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Adventurer {}
|
||||
44
tavern/src/converter.rs
Normal file
44
tavern/src/converter.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use pulldown_cmark::{html, Parser};
|
||||
|
||||
|
||||
pub enum Converter {}
|
||||
|
||||
|
||||
|
||||
impl Converter
|
||||
{
|
||||
/// Private function to handle the core conversion logic
|
||||
pub fn markdown_to_html(markdown_content: &str) -> String
|
||||
{
|
||||
let parser = Parser::new(markdown_content);
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
html_output
|
||||
}
|
||||
}
|
||||
|
||||
// tests/integration_test.rs
|
||||
// This file would be in your project's `tests` directory.
|
||||
// It tests the public functions of your main program.
|
||||
// You will also need to add `use crate::*` to access the functions.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_conversion()
|
||||
{
|
||||
// Create a temporary Markdown file for the test
|
||||
let markdown_content = "# Hello, World!\n\nThis is a **test**.";
|
||||
let expected_html =
|
||||
"<h1>Hello, World!</h1>\n<p>This is a <strong>test</strong>.</p>\n";
|
||||
|
||||
// Use the new Converter API
|
||||
let converted_html = Converter::markdown_to_html(&markdown_content);
|
||||
|
||||
// Assert that the output matches the expected HTML
|
||||
assert_eq!(converted_html.trim(), expected_html.trim());
|
||||
}
|
||||
}
|
||||
404
tavern/src/database.rs
Normal file
404
tavern/src/database.rs
Normal file
@ -0,0 +1,404 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use sqlx::{Error, Result};
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::adventurer::{Adventurer, Legend};
|
||||
use crate::tale::Tale;
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
use crate::converter::Converter;
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
use crate::tale::Lore;
|
||||
|
||||
|
||||
|
||||
/// Represents the database connection pool.
|
||||
pub struct Database
|
||||
{
|
||||
pool: SqlitePool
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Database
|
||||
{
|
||||
/// Opens a connection to the SQLite database pool and creates the necessary
|
||||
/// tables.
|
||||
///
|
||||
/// db_path is an absolute path to the resource file.
|
||||
/// Example:
|
||||
/// ```text
|
||||
/// open("/var/website/tavern.db");
|
||||
/// ```
|
||||
pub async fn open<P>(db_path: P) -> Result<Self>
|
||||
where P: AsRef<Path>
|
||||
{
|
||||
let db_str =
|
||||
db_path.as_ref().to_str().ok_or_else(|| {
|
||||
Error::Configuration("Invalid UTF-8 in database \
|
||||
path"
|
||||
.into())
|
||||
})?;
|
||||
|
||||
let url: String = format!("sqlite:///{db_str}");
|
||||
|
||||
// Set up connection options with foreign keys enabled.
|
||||
#[cfg(feature = "publisher")]
|
||||
let connect_options =
|
||||
SqliteConnectOptions::from_str(&url)?.read_only(false)
|
||||
.foreign_keys(true)
|
||||
.create_if_missing(true);
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
let connect_options =
|
||||
SqliteConnectOptions::from_str(&url)?.read_only(true)
|
||||
.foreign_keys(true)
|
||||
.create_if_missing(false);
|
||||
|
||||
let pool = SqlitePoolOptions::new().connect_with(connect_options)
|
||||
.await?;
|
||||
|
||||
let database = Database { pool };
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
database.create_tables().await?;
|
||||
|
||||
Ok(database)
|
||||
}
|
||||
|
||||
/// Creates the 'tales', 'adventurers', 'tags', 'tale_tags', and 'tavern'
|
||||
/// tables if they don't exist.
|
||||
#[cfg(feature = "publisher")]
|
||||
async fn create_tables(&self) -> Result<()>
|
||||
{
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS adventurers (
|
||||
handle TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
profile TEXT NOT NULL,
|
||||
image TEXT NOT NULL,
|
||||
blurb TEXT NOT NULL
|
||||
)"
|
||||
).execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS tales (
|
||||
slug TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
publish_date TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)"
|
||||
).execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)"
|
||||
).execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS tale_tags (
|
||||
tale_slug TEXT,
|
||||
tag_id INTEGER,
|
||||
FOREIGN KEY(tale_slug) REFERENCES tales(slug),
|
||||
FOREIGN KEY(tag_id) REFERENCES tags(id),
|
||||
UNIQUE(tale_slug, tag_id)
|
||||
)"
|
||||
).execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS tavern (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)"
|
||||
).execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts a single tale into the database.
|
||||
#[cfg(feature = "publisher")]
|
||||
pub async fn insert_tale(&self, tale: &Tale)
|
||||
-> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
// Convert the tales content from Markdown to HTML.
|
||||
let markdown_content = std::fs::read_to_string(&tale.story)?;
|
||||
let html_content: String = Converter::markdown_to_html(&markdown_content);
|
||||
|
||||
// Start a transaction.
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
// Store the tale.
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO tales (
|
||||
slug, title, author, summary, publish_date, content
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
tale.lore.slug,
|
||||
tale.lore.title,
|
||||
tale.lore.author,
|
||||
tale.lore.summary,
|
||||
tale.lore.publish_date,
|
||||
html_content
|
||||
).execute(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?;
|
||||
|
||||
// Store the tags.
|
||||
// For each tag ...
|
||||
for tag_name in &tale.lore.tags
|
||||
{
|
||||
// Insert a new tag, ignore if it already exists.
|
||||
sqlx::query!("INSERT OR IGNORE INTO tags (name) VALUES (?1)",
|
||||
tag_name).execute(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?;
|
||||
|
||||
// Get the tag_id for the newly inserted or existing tag.
|
||||
let id: i64 = sqlx::query!("SELECT id FROM tags WHERE name = ?1",
|
||||
tag_name).fetch_one(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?
|
||||
.id
|
||||
.unwrap_or(0); // Use unwrap_or to handle the Option<i64>
|
||||
|
||||
// Insert the tale_tag relationship.
|
||||
sqlx::query!(
|
||||
"INSERT OR IGNORE INTO tale_tags (tale_slug, tag_id)
|
||||
VALUES (?1, ?2)",
|
||||
tale.lore.slug,
|
||||
id
|
||||
).execute(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Commit the transaction.
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts a single adventurer into the database.
|
||||
#[cfg(feature = "publisher")]
|
||||
pub async fn insert_adventurer(&self, adventurer: &Adventurer)
|
||||
-> Result<()>
|
||||
{
|
||||
// Start a transaction.
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO adventurers (
|
||||
handle, name, profile, image, blurb
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
adventurer.handle,
|
||||
adventurer.name,
|
||||
adventurer.legend.profile,
|
||||
adventurer.legend.image,
|
||||
adventurer.legend.blurb
|
||||
).execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Commit the transaction.
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts the site-wide settings like title and description.
|
||||
#[cfg(feature = "publisher")]
|
||||
pub async fn insert_tavern(&self, title: &str, description: &str)
|
||||
-> Result<()>
|
||||
{
|
||||
// Start a transaction.
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
// Insert or replace the title.
|
||||
sqlx::query!("INSERT OR REPLACE INTO tavern (key, value) VALUES \
|
||||
('title', ?1)",
|
||||
title).execute(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?;
|
||||
|
||||
// Insert or replace the description.
|
||||
sqlx::query!("INSERT OR REPLACE INTO tavern (key, value) VALUES \
|
||||
('description', ?1)",
|
||||
description).execute(&mut *tx) // Pass mutable reference to the transaction
|
||||
.await?;
|
||||
|
||||
// Commit the transaction.
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
pub async fn get_tales_summary(&self, categories: &[String])
|
||||
-> Result<Vec<Lore>>
|
||||
{
|
||||
let mut tales = Vec::new();
|
||||
|
||||
// Start a read-only transaction.
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
// Dynamically build the query.
|
||||
let mut query = String::from(
|
||||
"SELECT
|
||||
t.title,
|
||||
t.slug,
|
||||
t.summary,
|
||||
t.author,
|
||||
t.publish_date,
|
||||
GROUP_CONCAT(tg.name, ',') AS tags
|
||||
FROM tales AS t
|
||||
LEFT JOIN tale_tags AS tt ON t.slug = tt.tale_slug
|
||||
LEFT JOIN tags AS tg ON tt.tag_id = tg.id"
|
||||
);
|
||||
|
||||
if !categories.is_empty()
|
||||
{
|
||||
query.push_str(" WHERE tg.name IN (");
|
||||
let placeholders: Vec<_> =
|
||||
(0..categories.len()).map(|_| "?").collect();
|
||||
query.push_str(&placeholders.join(", "));
|
||||
query.push(')');
|
||||
}
|
||||
|
||||
query.push_str(" GROUP BY t.slug ORDER BY t.publish_date DESC");
|
||||
|
||||
let mut q = sqlx::query(&query);
|
||||
for cat in categories
|
||||
{
|
||||
q = q.bind(cat);
|
||||
}
|
||||
|
||||
let rows = q.fetch_all(&mut *tx).await?;
|
||||
|
||||
for row in rows
|
||||
{
|
||||
let tags_str: Option<String> = row.try_get("tags")?;
|
||||
let tags = tags_str.map(|s| s.split(',').map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let date_str: String = row.try_get("publish_date")?;
|
||||
let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S")
|
||||
.map_err(|e| sqlx::Error::Decode(e.into()))?;
|
||||
|
||||
tales.push(Lore { title: row.try_get("title")?,
|
||||
slug: row.try_get("slug")?,
|
||||
summary: row.try_get("summary")?,
|
||||
author: row.try_get("author")?,
|
||||
publish_date,
|
||||
tags });
|
||||
}
|
||||
|
||||
tx.commit().await?; // Explicit commit, even for read transactions.
|
||||
|
||||
Ok(tales)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
pub async fn get_tale_by_slug(&self, slug: &str) -> Result<Option<Tale>>
|
||||
{
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
let tale_row = sqlx::query(
|
||||
"SELECT
|
||||
t.title, t.slug, t.summary, t.author, t.publish_date,
|
||||
t.content,
|
||||
GROUP_CONCAT(tg.name, ',') AS tags
|
||||
FROM tales AS t
|
||||
LEFT JOIN tale_tags AS tt ON t.slug = tt.tale_slug
|
||||
LEFT JOIN tags AS tg ON tt.tag_id = tg.id
|
||||
WHERE t.slug = ?1
|
||||
GROUP BY t.slug"
|
||||
).bind(slug)
|
||||
.fetch_optional(&mut *tx) // Use transaction here
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if let Some(row) = tale_row
|
||||
{
|
||||
let tags_str: Option<String> = row.try_get("tags")?;
|
||||
let tags = tags_str.map(|s| s.split(',').map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let date_str: String = row.try_get("publish_date")?;
|
||||
let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S")
|
||||
.map_err(|e| Error::Decode(e.into()))?;
|
||||
|
||||
let lore = Lore { title: row.try_get("title")?,
|
||||
slug: row.try_get("slug")?,
|
||||
summary: row.try_get("summary")?,
|
||||
author: row.try_get("author")?,
|
||||
publish_date,
|
||||
tags };
|
||||
|
||||
Ok(Some(Tale { lore,
|
||||
story: row.try_get("content")? }))
|
||||
}
|
||||
else
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
pub async fn get_adventurer(&self, handle: &str)
|
||||
-> Result<Option<Adventurer>>
|
||||
{
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
let legend = sqlx::query_as!(
|
||||
Legend,
|
||||
"SELECT
|
||||
profile AS profile,
|
||||
image AS image,
|
||||
blurb AS blurb
|
||||
FROM adventurers
|
||||
WHERE handle = ?1",
|
||||
handle
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let hero = sqlx::query!(
|
||||
"SELECT
|
||||
name,
|
||||
handle AS 'handle!'
|
||||
FROM adventurers
|
||||
WHERE handle = ?1",
|
||||
handle
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let adventurer = match (hero, legend)
|
||||
{
|
||||
(Some(h), Some(l)) =>
|
||||
{
|
||||
Some(Adventurer
|
||||
{
|
||||
name: h.name,
|
||||
handle: h.handle,
|
||||
legend: l,
|
||||
})
|
||||
}
|
||||
|
||||
_ => { None }
|
||||
};
|
||||
|
||||
Ok(adventurer)
|
||||
}
|
||||
}
|
||||
33
tavern/src/info.rs
Normal file
33
tavern/src/info.rs
Normal file
@ -0,0 +1,33 @@
|
||||
//! This is where the cargo build information can be retrieved from.
|
||||
|
||||
|
||||
|
||||
/// The environment variable defined by Cargo for the name.
|
||||
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
|
||||
|
||||
/// The environment variable defined by Cargo for the version.
|
||||
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// The string to display if a value is not defined during compile time.
|
||||
const NOT_DEFINED: &'static str = "UNDEFINED";
|
||||
|
||||
|
||||
|
||||
/// Returns the name of the program as defined by the CARGO_PKG_NAME. This is
|
||||
/// set at compile time and comes from the Cargo.toml file.
|
||||
///
|
||||
/// If a value is not found, then it will return the not defined value.
|
||||
pub fn get_name() -> &'static str
|
||||
{
|
||||
NAME.unwrap_or(NOT_DEFINED)
|
||||
}
|
||||
|
||||
|
||||
/// Returns the version of the program as defined by the CARGO_PKG_VERSION.
|
||||
/// This is set at compile time and comes from the Cargo.toml file.
|
||||
///
|
||||
/// If a value is not found, then it will return the not defined value.
|
||||
pub fn get_version() -> &'static str
|
||||
{
|
||||
VERSION.unwrap_or(NOT_DEFINED)
|
||||
}
|
||||
19
tavern/src/lib.rs
Normal file
19
tavern/src/lib.rs
Normal file
@ -0,0 +1,19 @@
|
||||
//! A blogging system that will allow you to write your blog in Markdown and
|
||||
//! then display it in HTML using Dioxus.
|
||||
|
||||
mod info;
|
||||
|
||||
mod adventurer;
|
||||
#[cfg(feature = "publisher")]
|
||||
mod converter;
|
||||
mod database;
|
||||
mod tale;
|
||||
mod tavern;
|
||||
|
||||
|
||||
|
||||
pub use crate::adventurer::{Adventurer, Legend};
|
||||
pub use crate::database::Database;
|
||||
pub use crate::info::{get_name, get_version};
|
||||
pub use crate::tale::{Lore, Tale};
|
||||
pub use crate::tavern::Tavern;
|
||||
56
tavern/src/tale.rs
Normal file
56
tavern/src/tale.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
|
||||
/// A type alias representing the path to a Markdown file.
|
||||
/// This type is used to point to the location of the content of a `Tale`.
|
||||
#[cfg(feature = "publisher")]
|
||||
pub type Markdown = std::path::PathBuf;
|
||||
|
||||
|
||||
/// A type alias representing the HTML content of the tale.
|
||||
#[cfg(not(feature = "publisher"))]
|
||||
pub type Markdown = String;
|
||||
|
||||
|
||||
|
||||
/// Metadata describing a tale.
|
||||
///
|
||||
/// This includes details such as the title, author, summary, and
|
||||
/// associated tags.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Lore
|
||||
{
|
||||
/// The title of the tale.
|
||||
pub title: String,
|
||||
|
||||
/// A URL-friendly version of the title, used for routing and linking.
|
||||
pub slug: String,
|
||||
|
||||
/// The name of the author who wrote the tale.
|
||||
pub author: String,
|
||||
|
||||
/// A short summary or description of the tale.
|
||||
pub summary: String,
|
||||
|
||||
/// A list of tags associated with the tale for categorization and
|
||||
/// searching.
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// The Date and Time that must elapse before this tale can be told.
|
||||
pub publish_date: NaiveDateTime
|
||||
}
|
||||
|
||||
|
||||
/// Represents a post or story in the application.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Tale
|
||||
{
|
||||
/// Metadata of the post.
|
||||
#[serde(flatten)]
|
||||
pub lore: Lore,
|
||||
|
||||
/// The file path to the Markdown content of the tale.
|
||||
pub story: Markdown
|
||||
}
|
||||
27
tavern/src/tavern.rs
Normal file
27
tavern/src/tavern.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::adventurer::Adventurer;
|
||||
use crate::tale::Tale;
|
||||
|
||||
|
||||
|
||||
/// Represents the entire blog or collection of content.
|
||||
///
|
||||
/// A `Tavern` contains a list of all tales (posts) and their respective
|
||||
/// authors. It serves as the central structure for organizing and serializing
|
||||
/// the site's content.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Tavern
|
||||
{
|
||||
///
|
||||
pub title: String,
|
||||
|
||||
///
|
||||
pub description: String,
|
||||
|
||||
/// A list of all published tales (posts) in the tavern.
|
||||
pub tales: Vec<Tale>,
|
||||
|
||||
/// A list of all adventurers (authors) who have contributed tales.
|
||||
pub authors: Vec<Adventurer>
|
||||
}
|
||||
102
tavern/tests/serde.rs
Normal file
102
tavern/tests/serde.rs
Normal file
@ -0,0 +1,102 @@
|
||||
#![cfg(feature = "publisher")]
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use tavern::{Adventurer, FrontMatter, Tale, Tavern};
|
||||
|
||||
|
||||
|
||||
fn generate_tavern() -> Tavern
|
||||
{
|
||||
let author: Adventurer =
|
||||
Adventurer { name: String::from("Jason Smith"),
|
||||
handle: String::from("myrddin"),
|
||||
profile:
|
||||
String::from("https://cybermages.tech/about/myrddin"),
|
||||
image:
|
||||
String::from("https://cybermages.tech/about/myrddin/pic"),
|
||||
blurb: String::from("I love code!") };
|
||||
|
||||
|
||||
let fm: FrontMatter = FrontMatter {
|
||||
title: String::from("Test post"),
|
||||
slug: String::from("test_post"),
|
||||
author: author.handle.clone(),
|
||||
summary: String::from("The Moon is made of cheese!"),
|
||||
tags: vec![String::from("Space"), String::from("Cheese")],
|
||||
publish_date:
|
||||
NaiveDate::from_ymd_opt(2025, 12, 25).unwrap()
|
||||
.and_hms_opt(13, 10, 41)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let tale: Tale = Tale {
|
||||
front_matter: fm,
|
||||
content: std::path::PathBuf::from("posts/test_post.md") };
|
||||
|
||||
Tavern { title: String::from("Runes & Ramblings"),
|
||||
description: String::from("Join software engineer Jason Smith \
|
||||
on his Rust programming journey. \
|
||||
Explore program design, tech \
|
||||
stacks, and more on this blog from \
|
||||
CybeMages, LLC."),
|
||||
tales: vec![tale],
|
||||
authors: vec![author] }
|
||||
}
|
||||
|
||||
|
||||
fn cleanup_temp_file(path: &std::path::PathBuf)
|
||||
{
|
||||
if path.exists()
|
||||
{
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn to_file()
|
||||
{
|
||||
let tavern = generate_tavern();
|
||||
|
||||
let toml_string = toml::to_string_pretty(&tavern).expect("Serialization \
|
||||
to TOML should \
|
||||
succeed");
|
||||
|
||||
// Save the TOML to a temporary file.
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push("tavern_test_out.toml");
|
||||
std::fs::write(&path, &toml_string).expect("Failed to write TOML to file");
|
||||
|
||||
cleanup_temp_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_file()
|
||||
{
|
||||
let tavern = generate_tavern();
|
||||
|
||||
let toml_string = toml::to_string_pretty(&tavern).expect("Serialization \
|
||||
to TOML should \
|
||||
succeed");
|
||||
|
||||
// Save the TOML to a temporary file.
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push("tavern_test_in.toml");
|
||||
std::fs::write(&path, &toml_string).expect("Failed to write TOML to file");
|
||||
|
||||
// Read the previously written TOML file
|
||||
let toml_data =
|
||||
std::fs::read_to_string(&path).expect("Failed to read TOML file");
|
||||
|
||||
// Deserialize it
|
||||
let tavern: Tavern =
|
||||
toml::from_str(&toml_data).expect("Failed to parse TOML");
|
||||
|
||||
// Assert some known values to make this a real test
|
||||
let tale = &tavern.tales[0];
|
||||
assert_eq!(tale.front_matter.title, "Test post");
|
||||
assert_eq!(tale.front_matter.slug, "test_post");
|
||||
assert_eq!(tale.front_matter.author, "myrddin");
|
||||
|
||||
cleanup_temp_file(&path);
|
||||
}
|
||||
Reference in New Issue
Block a user