Experimentally transform markdown for better presentation in full text search results
For issue #37
This commit is contained in:
parent
e499a095c7
commit
b1e598cb17
10 changed files with 80 additions and 12 deletions
|
@ -37,7 +37,7 @@ function debouncer(interval, callback) {
|
||||||
|
|
||||||
const query = input.value;
|
const query = input.value;
|
||||||
fetch(
|
fetch(
|
||||||
"_search?snippet_size=4&limit=4&q=" + encodeURIComponent(query),
|
"_search?snippet_size=10&limit=4&q=" + encodeURIComponent(query),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
|
@ -56,7 +56,7 @@ function debouncer(interval, callback) {
|
||||||
item.querySelector('.link').href = hit.slug || ".";
|
item.querySelector('.link').href = hit.slug || ".";
|
||||||
item.querySelector('.link').setAttribute("data-focusindex", index + 1);
|
item.querySelector('.link').setAttribute("data-focusindex", index + 1);
|
||||||
item.querySelector('.title').textContent = hit.title;
|
item.querySelector('.title').textContent = hit.title;
|
||||||
item.querySelector('.snippet').textContent = hit.snippet;
|
item.querySelector('.snippet').innerHTML = hit.snippet;
|
||||||
results.appendChild(item);
|
results.appendChild(item);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -305,9 +305,6 @@ article ul.search-results {
|
||||||
.search-result .title {
|
.search-result .title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.snippet {
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
.search-result p {
|
.search-result p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
16
build.rs
16
build.rs
|
@ -11,6 +11,11 @@ use std::io::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
fn markdown_to_fts(_: &::diesel::sqlite::Context) -> CString {
|
||||||
|
panic!("Should never be called when running migrations on build.db")
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR");
|
let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR");
|
||||||
let db_path = Path::new(&out_dir).join("build.db");
|
let db_path = Path::new(&out_dir).join("build.db");
|
||||||
|
@ -18,14 +23,21 @@ fn main() {
|
||||||
|
|
||||||
let _ignore_failure = std::fs::remove_file(db_path);
|
let _ignore_failure = std::fs::remove_file(db_path);
|
||||||
|
|
||||||
let connection = SqliteConnection::establish(db_path)
|
let mut connection = SqliteConnection::establish(db_path)
|
||||||
.expect(&format!("Error esablishing a database connection to {}", db_path));
|
.expect(&format!("Error esablishing a database connection to {}", db_path));
|
||||||
|
|
||||||
// Integer is a dummy placeholder. Compiling fails when passing ().
|
// Integer is a dummy placeholder. Compiling fails when passing ().
|
||||||
diesel::expression::sql_literal::sql::<(diesel::types::Integer)>("PRAGMA foreign_keys = ON")
|
diesel::expression::sql_literal::sql::<(diesel::sql_types::Integer)>("PRAGMA foreign_keys = ON")
|
||||||
.execute(&connection)
|
.execute(&connection)
|
||||||
.expect("Should be able to enable foreign keys");
|
.expect("Should be able to enable foreign keys");
|
||||||
|
|
||||||
|
connection.create_scalar_function(
|
||||||
|
"markdown_to_fts",
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
markdown_to_fts,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
diesel_migrations::run_pending_migrations(&connection).unwrap();
|
diesel_migrations::run_pending_migrations(&connection).unwrap();
|
||||||
|
|
||||||
let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs");
|
let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs");
|
||||||
|
|
24
migrations/20180119150706_convert_markdown_for_fts/up.sql
Normal file
24
migrations/20180119150706_convert_markdown_for_fts/up.sql
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
DROP TRIGGER article_revisions_ai;
|
||||||
|
DROP TRIGGER article_revisions_ad;
|
||||||
|
DROP TRIGGER article_revisions_au_disable;
|
||||||
|
DROP TRIGGER article_revisions_au_enable;
|
||||||
|
|
||||||
|
CREATE TRIGGER article_revisions_ai AFTER INSERT ON article_revisions WHEN new.latest = 1 BEGIN
|
||||||
|
DELETE FROM article_search WHERE rowid = new.article_id;
|
||||||
|
INSERT INTO article_search(rowid, title, body, slug) VALUES (new.article_id, new.title, markdown_to_fts(new.body), new.slug);
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER article_revisions_ad AFTER DELETE ON article_revisions WHEN old.latest = 1 BEGIN
|
||||||
|
DELETE FROM article_search WHERE rowid = old.article_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Index unique_latest_revision_per_article_id makes sure the following is sufficient:
|
||||||
|
CREATE TRIGGER article_revisions_au_disable AFTER UPDATE ON article_revisions WHEN old.latest = 1 AND new.latest = 0 BEGIN
|
||||||
|
DELETE FROM article_search WHERE rowid = old.article_id;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER article_revisions_au_enable AFTER UPDATE ON article_revisions WHEN old.latest = 0 AND new.latest = 1 BEGIN
|
||||||
|
INSERT INTO article_search(rowid, title, body, slug) VALUES (new.article_id, new.title, markdown_to_fts(new.body), new.slug);
|
||||||
|
END;
|
||||||
|
|
||||||
|
DELETE FROM article_search;
|
||||||
|
INSERT INTO article_search(title, body, slug)
|
||||||
|
SELECT title, markdown_to_fts(body), slug FROM article_revisions WHERE latest = 1;
|
19
src/db.rs
19
src/db.rs
|
@ -9,12 +9,27 @@ embed_migrations!();
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SqliteInitializer;
|
struct SqliteInitializer;
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
fn markdown_to_fts(ctx: &::diesel::sqlite::Context) -> CString {
|
||||||
|
use rendering;
|
||||||
|
CString::new(rendering::render_markdown_for_fts(&ctx.get::<String>(0))).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer {
|
impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer {
|
||||||
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> {
|
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> {
|
||||||
sql::<(Integer)>("PRAGMA foreign_keys = ON")
|
sql::<(Integer)>("PRAGMA foreign_keys = ON")
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.and(Ok(()))
|
.map_err(|x| r2d2_diesel::Error::QueryError(x))?;
|
||||||
.map_err(|x| r2d2_diesel::Error::QueryError(x))
|
|
||||||
|
conn.create_scalar_function(
|
||||||
|
"markdown_to_fts",
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
markdown_to_fts,
|
||||||
|
).map_err(|x| r2d2_diesel::Error::QueryError(x))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use pulldown_cmark::{Parser, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML};
|
use pulldown_cmark::{Parser, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML};
|
||||||
|
use pulldown_cmark::Event::Text;
|
||||||
|
|
||||||
pub fn render_markdown(src: &str) -> String {
|
pub fn render_markdown(src: &str) -> String {
|
||||||
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
||||||
|
@ -7,3 +8,22 @@ pub fn render_markdown(src: &str) -> String {
|
||||||
html::push_html(&mut buf, p);
|
html::push_html(&mut buf, p);
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_markdown_for_fts(src: &str) -> String {
|
||||||
|
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
||||||
|
let p = Parser::new_ext(src, opts);
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
for event in p {
|
||||||
|
match event {
|
||||||
|
Text(text) => buf.push_str(&text),
|
||||||
|
_ => buf.push_str(" "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.replace('&', "");
|
||||||
|
buf.replace('<', "");
|
||||||
|
buf.replace('>', "");
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use state::State;
|
||||||
use web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
const DEFAULT_LIMIT: u32 = 10;
|
const DEFAULT_LIMIT: u32 = 10;
|
||||||
const DEFAULT_SNIPPET_SIZE: u32 = 8;
|
const DEFAULT_SNIPPET_SIZE: u32 = 25;
|
||||||
|
|
||||||
type BoxResource = Box<Resource + Sync + Send>;
|
type BoxResource = Box<Resource + Sync + Send>;
|
||||||
|
|
||||||
|
|
|
@ -386,7 +386,7 @@ impl<'a> SyncState<'a> {
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
sql_query(
|
sql_query(
|
||||||
"SELECT title, snippet(article_search, 1, '', '', '\u{2026}', ?) AS snippet, slug \
|
"SELECT title, snippet(article_search, 1, '<em>', '</em>', '\u{2026}', ?) AS snippet, slug \
|
||||||
FROM article_search \
|
FROM article_search \
|
||||||
WHERE article_search MATCH ? \
|
WHERE article_search MATCH ? \
|
||||||
ORDER BY rank \
|
ORDER BY rank \
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<ul class="search-results default-keyboard-focus-control">
|
<ul class="search-results default-keyboard-focus-control">
|
||||||
{{#hits}}
|
{{#hits}}
|
||||||
<li class="search-result"><a data-focusindex="{{.0}}" class="link" href="{{.1.link()}}"><p class="title">{{.1.title}}</p><p class="snippet">{{.1.snippet}}</p></a></li>
|
<li class="search-result"><a data-focusindex="{{.0}}" class="link" href="{{.1.link()}}"><p class="title">{{.1.title}}</p><p class="snippet">{{{.1.snippet}}}</p></a></li>
|
||||||
{{/hits}}
|
{{/hits}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/hits}}
|
{{/hits}}
|
||||||
|
|
Loading…
Reference in a new issue