Browse Source

Image Markup

master
Inderjit Gill 8 months ago
parent
commit
f599c89422
15 changed files with 247 additions and 69 deletions
  1. +5
    -0
      core/src/compiler.rs
  2. +1
    -0
      core/src/element.rs
  3. +73
    -2
      core/src/lexer.rs
  4. +106
    -0
      core/src/parser.rs
  5. +0
    -12
      misc/db/schema.psql
  6. +1
    -2
      server/src/api.rs
  7. +1
    -1
      server/src/db/mod.rs
  8. +11
    -6
      server/src/db/uploader.rs
  9. +12
    -19
      server/src/handler/uploader.rs
  10. +1
    -1
      server/src/interop/mod.rs
  11. +3
    -0
      server/src/lib.rs
  12. +4
    -0
      user-content/.gitignore
  13. +2
    -7
      wasm/src/lib.rs
  14. +1
    -1
      www/js/components/ImageWidget.js
  15. +26
    -18
      www/js/components/Note.js

+ 5
- 0
core/src/compiler.rs View File

@ -69,6 +69,11 @@ fn compile_node_to_struct(node: &Node, key: usize) -> Result<Vec<Element>> {
text: Some(String::from(text)),
..Default::default()
}],
Node::Image(src) => vec![Element {
name: String::from("img"),
src: Some(String::from(src)),
..Default::default()
}],
Node::Underlined(ns) => element_key_class("span", key, "underlined", ns)?,
Node::UnorderedList(ns) => element_key("ul", key, ns)?,
};


+ 1
- 0
core/src/element.rs View File

@ -25,6 +25,7 @@ pub struct Element {
pub html_for: Option<String>,
pub href: Option<String>,
pub src: Option<String>,
pub html_type: Option<String>,
pub id: Option<String>,


+ 73
- 2
core/src/lexer.rs View File

@ -22,6 +22,7 @@ use strum_macros::EnumDiscriminants;
#[strum_discriminants(name(TokenIdent))]
pub enum Token<'a> {
Asterisk,
At,
BackTick,
BracketEnd,
BracketStart,
@ -32,6 +33,8 @@ pub enum Token<'a> {
Hash,
Hyphen,
Newline,
ParenEnd,
ParenStart,
Period,
Pipe,
Text(&'a str),
@ -43,6 +46,7 @@ pub enum Token<'a> {
pub(crate) fn get_token_value<'a>(token: &'a Token) -> &'a str {
match token {
Token::Asterisk => "*",
Token::At => "@",
Token::BackTick => "`",
Token::BracketEnd => "]",
Token::BracketStart => "[",
@ -53,6 +57,8 @@ pub(crate) fn get_token_value<'a>(token: &'a Token) -> &'a str {
Token::Hash => "#",
Token::Hyphen => "-",
Token::Newline => "\n",
Token::ParenEnd => ")",
Token::ParenStart => "(",
Token::Period => ".",
Token::Pipe => "|",
Token::Text(s) => s,
@ -74,6 +80,7 @@ pub fn tokenize(s: &str) -> Result<Vec<Token>> {
if let Some(ch) = input.chars().next() {
let (token, size) = match ch {
'*' => (Token::Asterisk, 1),
'@' => (Token::At, 1),
'`' => (Token::BackTick, 1),
'[' => (Token::BracketStart, 1),
']' => (Token::BracketEnd, 1),
@ -83,6 +90,8 @@ pub fn tokenize(s: &str) -> Result<Vec<Token>> {
'#' => (Token::Hash, 1),
'-' => (Token::Hyphen, 1),
'\n' => (Token::Newline, 1),
'(' => (Token::ParenStart, 1),
')' => (Token::ParenEnd, 1),
'.' => (Token::Period, 1),
'|' => (Token::Pipe, 1),
'_' => (Token::Underscore, 1),
@ -136,7 +145,7 @@ fn eat_text(input: &str) -> Result<(Token, usize)> {
fn is_text(ch: char) -> bool {
match ch {
'\n' | '[' | ']' | '_' | '*' | '`' | '^' | '~' | '"' | '|' | '#' => false,
'\n' | '[' | ']' | '(' | ')' | '@' | '_' | '*' | '`' | '^' | '~' | '"' | '|' | '#' => false,
_ => true,
}
}
@ -158,16 +167,78 @@ mod tests {
tok("5", &[Token::Digits("5"), Token::EOS]);
tok(
"foo *bar* 456789",
"foo *bar* @ 456789",
&[
Token::Text("foo "),
Token::Asterisk,
Token::Text("bar"),
Token::Asterisk,
Token::Whitespace(" "),
Token::At,
Token::Whitespace(" "),
Token::Digits("456789"),
Token::EOS,
],
);
}
#[test]
fn test_lexer_image_syntax() {
// the three kinds of lexed streams for representing fourc codes:
// fourc starts with digit, ends in letter
tok(
"@img(00a.jpg)",
&[
Token::At,
Token::Text("img"),
Token::ParenStart,
Token::Digits("00"),
Token::Text("a.jpg"),
Token::ParenEnd,
Token::EOS,
],
);
// fourc starts with digit, ends in digit
tok(
"@img(000.jpg)",
&[
Token::At,
Token::Text("img"),
Token::ParenStart,
Token::Digits("000"),
Token::Period,
Token::Text("jpg"),
Token::ParenEnd,
Token::EOS,
],
);
// fourc starts with letter, ends in digit
tok(
"@img(a00.jpg)",
&[
Token::At,
Token::Text("img"),
Token::ParenStart,
Token::Text("a00.jpg"),
Token::ParenEnd,
Token::EOS,
],
);
// fourc starts with letter, ends in letter
tok(
"@img(abc.jpg)",
&[
Token::At,
Token::Text("img"),
Token::ParenStart,
Token::Text("abc.jpg"),
Token::ParenEnd,
Token::EOS,
],
);
}
}

+ 106
- 0
core/src/parser.rs View File

@ -44,6 +44,7 @@ pub enum Node {
Text(String),
Underlined(Vec<Node>),
UnorderedList(Vec<Node>),
Image(String),
}
pub(crate) fn is_numbered_list_item(tokens: &'_ [Token]) -> bool {
@ -175,6 +176,7 @@ fn eat_item_until<'a>(tokens: &'a [Token], halt_at: TokenIdent) -> ParserResult<
fn eat_item<'a>(tokens: &'a [Token]) -> ParserResult<'a, Node> {
match tokens[0] {
Token::At => eat_at(tokens),
Token::Asterisk => eat_matching_pair(tokens, TokenIdent::Asterisk, NodeIdent::Strong),
Token::BackTick => eat_codeblock(tokens),
Token::BracketEnd => eat_text_including(tokens),
@ -249,6 +251,56 @@ fn eat_pipe<'a>(mut tokens: &'a [Token<'a>]) -> ParserResult<Node> {
}
}
fn is_at_specifier<'a>(tokens: &'a [Token<'a>]) -> bool {
is_token_at_index(tokens, 0, TokenIdent::At)
&& is_token_at_index(tokens, 1, TokenIdent::Text)
&& is_token_at_index(tokens, 2, TokenIdent::ParenStart)
}
fn is_at_img_specifier<'a>(tokens: &'a [Token<'a>]) -> bool {
if is_token_at_index(tokens, 1, TokenIdent::Text) && is_token_at_index(tokens, 2, TokenIdent::ParenStart) {
if is_token_at_index(tokens, 3, TokenIdent::Text) && is_token_at_index(tokens, 4, TokenIdent::ParenEnd) {
// e.g. @img(abc.jpg)
true
} else if is_token_at_index(tokens, 3, TokenIdent::Digits)
&& is_token_at_index(tokens, 4, TokenIdent::Text)
&& is_token_at_index(tokens, 5, TokenIdent::ParenEnd)
{
// e.g. @img(00a.jpg)
true
} else if is_token_at_index(tokens, 3, TokenIdent::Digits)
&& is_token_at_index(tokens, 4, TokenIdent::Period)
&& is_token_at_index(tokens, 5, TokenIdent::Text)
&& is_token_at_index(tokens, 6, TokenIdent::ParenEnd)
{
// e.g. @img(000.jpg)
true
} else {
false
}
} else {
false
}
}
fn eat_at<'a>(mut tokens: &'a [Token<'a>]) -> ParserResult<Node> {
if is_at_specifier(tokens) {
match tokens[1] {
Token::Text("img") => {
if is_at_img_specifier(tokens) {
tokens = &tokens[3..]; // eat the '@img('
let (mut tokens, image_name) = eat_string(tokens, TokenIdent::ParenEnd)?;
tokens = &tokens[1..]; // eat the ')'
return Ok((tokens, Node::Image(image_name.to_string())));
}
}
_ => (),
}
}
eat_text_including(tokens)
}
fn eat_matching_pair<'a>(tokens: &'a [Token<'a>], halt_at: TokenIdent, node_ident: NodeIdent) -> ParserResult<Node> {
if remaining_tokens_contain(tokens, halt_at) {
let (toks, children) = eat_list(tokens, halt_at)?;
@ -412,6 +464,9 @@ fn eat_text_as_string<'a>(mut tokens: &'a [Token<'a>]) -> ParserResult<String> {
Token::Whitespace(s) => value += s,
Token::Period => value += ".",
Token::Hyphen => value += "-",
// Token::At => value += "@",
Token::ParenStart => value += "(",
Token::ParenEnd => value += ")",
_ => return Ok((tokens, value)),
}
tokens = &tokens[1..];
@ -525,6 +580,13 @@ mod tests {
};
}
fn assert_image(node: &Node, expected: &'static str) {
match node {
Node::Image(s) => assert_eq!(s, expected),
_ => assert_eq!(false, true),
};
}
fn assert_strong1(node: &Node, expected: &'static str) {
match node {
Node::Strong(ns) => {
@ -1049,4 +1111,48 @@ here is the closing paragraph",
res = skip_leading_whitespace_and_newlines(&toks).unwrap();
assert_eq!(5, res.len());
}
#[test]
fn test_at_image() {
{
let nodes = build("@img(abc.jpg)");
assert_eq!(1, nodes.len());
let children = paragraph_children(&nodes[0]).unwrap();
assert_eq!(children.len(), 1);
assert_image(&children[0], "abc.jpg");
}
{
let nodes = build("@img(a00.jpg)");
assert_eq!(1, nodes.len());
let children = paragraph_children(&nodes[0]).unwrap();
assert_eq!(children.len(), 1);
assert_image(&children[0], "a00.jpg");
}
{
let nodes = build("@img(00a.jpg)");
assert_eq!(1, nodes.len());
let children = paragraph_children(&nodes[0]).unwrap();
assert_eq!(children.len(), 1);
assert_image(&children[0], "00a.jpg");
}
{
let nodes = build("@img(000.jpg)");
assert_eq!(1, nodes.len());
let children = paragraph_children(&nodes[0]).unwrap();
assert_eq!(children.len(), 1);
assert_image(&children[0], "000.jpg");
}
}
}

+ 0
- 12
misc/db/schema.psql View File

@ -99,15 +99,3 @@ CREATE TABLE IF NOT EXISTS images (
filename TEXT NOT NULL
);
-- name will be a 4 character id
-- path will be ${name}.${fileextension}
-- url will be img/${user_id}/${path}
-- location on disk will be img/${user_id}/${path}
-- id in markup will be: @img-${name} e.g. this is bioleninism: @img-0001
-- how are img display parameters specified in the markup?
-- this is bioleninism: @img-0001{width=100,caption="what the fuck"}
-- this is bioleninism: @ref-673{RefToParent}

+ 1
- 2
server/src/api.rs View File

@ -108,9 +108,8 @@ pub fn public_api(mount_point: &str) -> actix_web::Scope {
scope("/upload")
.route("", post().to(uploader::create)) // upload images
.route("", get().to(uploader::get)) // get this user's most recent uploads
.route("directory", get().to(uploader::get_directory)) // this user's upload directory
.route("directory", get().to(uploader::get_directory)), // this user's upload directory
)
}
pub fn bad_request<B>(res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {


+ 1
- 1
server/src/db/mod.rs View File

@ -28,5 +28,5 @@ mod pg;
pub mod points;
pub mod publications;
pub mod ref_kind;
pub mod users;
pub mod uploader;
pub mod users;

+ 11
- 6
server/src/db/uploader.rs View File

@ -41,7 +41,6 @@ impl From<UserImageCount> for i32 {
}
}
#[derive(Deserialize, PostgresMapper, Serialize)]
#[pg_mapper(table = "images")]
struct UserUploadedImage {
@ -51,14 +50,21 @@ struct UserUploadedImage {
impl From<UserUploadedImage> for interop::UserUploadedImage {
fn from(e: UserUploadedImage) -> interop::UserUploadedImage {
interop::UserUploadedImage {
filename: e.filename
filename: e.filename,
}
}
}
pub(crate) async fn get_recent(db_pool: &Pool, user_id: Key) -> Result<Vec<interop::UserUploadedImage>> {
pg::many_from::<UserUploadedImage, interop::UserUploadedImage>(db_pool, include_str!("sql/uploader_recent.sql"), &[&user_id])
.await
pub(crate) async fn get_recent(
db_pool: &Pool,
user_id: Key,
) -> Result<Vec<interop::UserUploadedImage>> {
pg::many_from::<UserUploadedImage, interop::UserUploadedImage>(
db_pool,
include_str!("sql/uploader_recent.sql"),
&[&user_id],
)
.await
}
pub(crate) async fn get_image_count(db_pool: &Pool, user_id: Key) -> Result<i32> {
@ -70,7 +76,6 @@ pub(crate) async fn get_image_count(db_pool: &Pool, user_id: Key) -> Result<i32>
.await
}
pub(crate) async fn set_image_count(db_pool: &Pool, user_id: Key, new_count: i32) -> Result<()> {
let mut client: Client = db_pool.get().await.map_err(Error::DeadPool)?;
let tx = client.transaction().await?;


+ 12
- 19
server/src/handler/uploader.rs View File

@ -15,22 +15,20 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::error::{Error, Result};
use crate::session;
use crate::db::uploader as db;
use std::path::Path;
use std::ffi::OsStr;
use std::path::Path;
use std::io::Write;
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use futures::{StreamExt, TryStreamExt};
use actix_web::web::Data;
use actix_web::{web, HttpResponse};
use deadpool_postgres::Pool;
use futures::{StreamExt, TryStreamExt};
use std::io::Write;
#[allow(unused_imports)]
use tracing::info;
@ -58,16 +56,16 @@ pub async fn create(
db_pool: Data<Pool>,
session: actix_session::Session,
) -> Result<HttpResponse> {
let user_id = session::user_id(&session)?;
// create the user specific directory
let user_directory = format!("../www/img/u/{}", user_id);
std::fs::DirBuilder::new().recursive(true).create(&user_directory)?;
let user_directory = format!("../user-content/{}", user_id);
std::fs::DirBuilder::new()
.recursive(true)
.create(&user_directory)?;
let mut user_image_count = db::get_image_count(&db_pool, user_id).await?;
// iterate over multipart stream
while let Ok(Some(mut field)) = payload.try_next().await {
let content_type = field.content_disposition().unwrap();
@ -83,7 +81,6 @@ pub async fn create(
user_image_count += 1;
db::set_image_count(&db_pool, user_id, user_image_count).await?;
let mut f = web::block(|| std::fs::File::create(filepath))
.await
.unwrap();
@ -97,23 +94,19 @@ pub async fn create(
// save the entry in the images table
db::add_image_entry(&db_pool, user_id, &derived_filename).await?;
}
Ok(HttpResponse::Ok().into())
}
fn get_extension(filename: &str) -> Option<&str> {
Path::new(filename)
.extension()
.and_then(OsStr::to_str)
Path::new(filename).extension().and_then(OsStr::to_str)
}
fn number_as_fourc(n: i32) -> Result<String> {
let res = format!("{:0>4}", format_radix(n as u32, 36)?);
let res = format!("{:0>3}", format_radix(n as u32, 36)?);
Ok(res)
}
fn format_radix(mut x: u32, radix: u32) -> Result<String> {
let mut result = vec![];
@ -137,7 +130,7 @@ mod tests {
#[test]
fn test_fourc_generation() {
assert_eq!(number_as_fourc(0).unwrap().to_string(), "0000");
assert_eq!(number_as_fourc(1234).unwrap().to_string(), "00ya");
assert_eq!(number_as_fourc(0).unwrap().to_string(), "000");
assert_eq!(number_as_fourc(1234).unwrap().to_string(), "0ya");
}
}

+ 1
- 1
server/src/interop/mod.rs View File

@ -24,8 +24,8 @@ pub mod notes;
pub mod people;
pub mod points;
pub mod publications;
pub mod users;
pub mod uploader;
pub mod users;
use std::fmt;


+ 3
- 0
server/src/lib.rs View File

@ -51,6 +51,8 @@ pub async fn start_server() -> Result<()> {
let port = env::var("PORT")?;
let www_path = env::var("WWW_PATH")?;
let user_content_path = String::from("../user-content");
let postgres_db = env::var("POSTGRES_DB")?;
let postgres_host = env::var("POSTGRES_HOST")?;
let postgres_user = env::var("POSTGRES_USER")?;
@ -90,6 +92,7 @@ pub async fn start_server() -> Result<()> {
.wrap(session_store)
.wrap(error_handlers)
.service(api::public_api("/api"))
.service(fs::Files::new("/u", String::from(&user_content_path)))
.service(fs::Files::new("/", String::from(&www_path)).index_file("index.html"))
})
.bind(format!("127.0.0.1:{}", port))?


+ 4
- 0
user-content/.gitignore View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

+ 2
- 7
wasm/src/lib.rs View File

@ -1,4 +1,3 @@
#![allow(dead_code)]
use cfg_if::cfg_if;
use wasm_bindgen::prelude::*;
@ -51,9 +50,7 @@ pub fn init_wasm() {
#[wasm_bindgen]
pub fn markup_as_struct(markup: &str) -> JsValue {
match core::markup_as_struct(markup) {
Ok(res) => {
JsValue::from_serde(&res).unwrap()
},
Ok(res) => JsValue::from_serde(&res).unwrap(),
Err(_) => {
error!("markup_compiler failed");
JsValue::from_serde(&"error").unwrap()
@ -64,9 +61,7 @@ pub fn markup_as_struct(markup: &str) -> JsValue {
#[wasm_bindgen]
pub fn markup_splitter(markup: &str) -> JsValue {
match core::split_markup(markup) {
Ok(res) => {
JsValue::from_serde(&res).unwrap()
},
Ok(res) => JsValue::from_serde(&res).unwrap(),
Err(_) => {
error!("markup_splitter failed");
JsValue::from_serde(&"error").unwrap()


+ 1
- 1
www/js/components/ImageWidget.js View File

@ -177,7 +177,7 @@ function drop(e) {
function ImageWidgetItem({ filename, imageDirectory }) {
return html`
<div class="image-widget-item">
<img class="image-widget-img" src="/img/u/${imageDirectory}/${filename}"/>
<img class="image-widget-img" src="/u/${imageDirectory}/${filename}"/>
<div class="image-widget-title">${filename}</div>
</div>`;
}


+ 26
- 18
www/js/components/Note.js View File

@ -169,7 +169,7 @@ export default function Note(props) {
return html`
<div class="note">
${ note.separator && html`<hr/>` }
${ isEditing ? buildEditableContent() : buildReadingContent(note, props.note.id, onShowButtonsClicked, props.note.decks) }
${ isEditing ? buildEditableContent() : buildReadingContent(note, props.note.id, onShowButtonsClicked, props.note.decks, state.imageDirectory) }
${ showModButtons && showAddDecksUI && buildAddDecksUI() }
${ showModButtons && !showAddDecksUI && buildMainButtons() }
</div>
@ -230,9 +230,9 @@ function buildNoteReference(marginConnections) {
});
};
function buildReadingContent(note, noteId, onShowButtonsClicked, decks) {
function buildReadingContent(note, noteId, onShowButtonsClicked, decks, imageDirectory) {
const noteRefContents = buildNoteReference(decks);
const contentMarkup = buildMarkup(note.content);
const contentMarkup = buildMarkup(note.content, imageDirectory);
return html`
<div>
@ -246,30 +246,38 @@ function buildReadingContent(note, noteId, onShowButtonsClicked, decks) {
</div>`;
};
// build the React structure from the AST generated by rust
// build the Preact structure from the AST generated by rust
//
function buildMarkup(content) {
function buildMarkup(content, imageDirectory) {
const wasmInterface = useWasmInterface();
const astArray = wasmInterface.asHtmlAst(content);
return astArray.map(compile);
}
function attrs(n) {
let res = {
key: n.key,
class: n.class_name,
for: n.html_for,
href: n.href,
type: n.html_type,
id: n.id
}
function compile(n) {
return n.name === "text" ? n.text : h(n.name, attrs(n), ...n.children.map(compile));
}
if (n.src) {
res.src = `/u/${imageDirectory}/${n.src}`;
}
return res;
}
function attrs(n) {
return {
key: n.key,
class: n.class_name,
for: n.html_for,
href: n.href,
type: n.html_type,
id: n.id
function compile(n) {
return n.name === "text" ? n.text : h(n.name, attrs(n), ...n.children.map(compile));
}
return astArray.map(compile);
}
function addDecks(propsNote, decks, onDecksChanged, dispatch) {
let data = {
note_id: propsNote.id,


Loading…
Cancel
Save