diff --git a/Cargo.toml b/Cargo.toml index b28eb781..98d6ed38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "lib/domain/libimagnotes", "lib/domain/libimagtimetrack", "lib/domain/libimagtodo", + "lib/domain/libimagwiki", "lib/entry/libimagentryannotation", "lib/entry/libimagentrycategory", "lib/entry/libimagentrydatetime", diff --git a/doc/src/05100-lib-wiki.md b/doc/src/05100-lib-wiki.md new file mode 100644 index 00000000..93ba6970 --- /dev/null +++ b/doc/src/05100-lib-wiki.md @@ -0,0 +1,59 @@ +## libimagwiki + +The wiki library implements a complete wiki for personal use. + +This basically is a note-taking functionality combined with linking. + +### Layout + +The basic structure and layout is as simple as it gets: + +`/wiki` holds all wikis. The default wiki is `/wiki/default`. Below that there +are entries. Entries can be in sub-collections, so +`/wiki/default/cars/mustang` could be an entry. + + +``` {.numberLines} + ++-------------+ +| | +| WikiStore | +| | ++------+------+ + 1 | + | + | n ++------v------+ +| | +| Wiki | +| | ++------+------+ + 1 | + | + | n ++------v------+ +| | n +| Entry <------+ +| | | ++------+------+ | + 1 | | + | | + | | + +-------------+ +``` + +The store offers an interface to get a Wiki. The wiki offers an interface to get +entries from it. + +Each Entry might link to a number of other entries _within the same wiki_. +Cross-linking from one wiki entry to an entry of another wiki is technically +possible, but not supported by the Entry itself (also read below). + +When creating a new wiki, the main page is automatically created. + +### Autolinking + +The `Entry` structure offers an interface which can be used to automatically +detect links in the markdown. +The links are then automatically linked (as in `libimagentrylink`). + diff --git a/lib/domain/libimagwiki/Cargo.toml b/lib/domain/libimagwiki/Cargo.toml new file mode 100644 index 00000000..2e0c07ba --- /dev/null +++ b/lib/domain/libimagwiki/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "libimagwiki" +version = "0.7.0" +authors = ["Matthias Beyer "] + +description = "Library for the imag core distribution" + +keywords = ["imag", "PIM", "personal", "information", "management"] +readme = "../../../README.md" +license = "LGPL-2.1" + +documentation = "https://matthiasbeyer.github.io/imag/imag_documentation/index.html" +repository = "https://github.com/matthiasbeyer/imag" +homepage = "http://imag-pim.org" + +[badges] +travis-ci = { repository = "matthiasbeyer/imag" } +is-it-maintained-issue-resolution = { repository = "matthiasbeyer/imag" } +is-it-maintained-open-issues = { repository = "matthiasbeyer/imag" } +maintenance = { status = "actively-developed" } + +[dependencies] +log = "0.4" +error-chain = "0.11" +toml = "0.4" +toml-query = "0.6" +filters = "0.2" + +libimagstore = { version = "0.7.0", path = "../../../lib/core/libimagstore" } +libimagerror = { version = "0.7.0", path = "../../../lib/core/libimagerror" } +libimagentrylink = { version = "0.7.0", path = "../../../lib/entry/libimagentrylink" } +libimagentrymarkdown = { version = "0.7.0", path = "../../../lib/entry/libimagentrymarkdown" } + diff --git a/lib/domain/libimagwiki/README.md b/lib/domain/libimagwiki/README.md new file mode 120000 index 00000000..8fe46f2b --- /dev/null +++ b/lib/domain/libimagwiki/README.md @@ -0,0 +1 @@ +../../../doc/src/05100-lib-wiki.md \ No newline at end of file diff --git a/lib/domain/libimagwiki/src/entry.rs b/lib/domain/libimagwiki/src/entry.rs new file mode 100644 index 00000000..39b9d805 --- /dev/null +++ b/lib/domain/libimagwiki/src/entry.rs @@ -0,0 +1,73 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2018 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagstore::store::Store; +use libimagstore::store::Entry; +use libimagentrymarkdown::processor::LinkProcessor; + +use error::Result; +use error::WikiErrorKind as WEK; +use error::ResultExt; + +pub trait WikiEntry { + fn autolink(&mut self, store: &Store) -> Result<()>; + fn autolink_with_processor(&mut self, store: &Store, processor: LinkProcessor) -> Result<()>; +} + +impl WikiEntry for Entry { + + /// Autolink entry to entries linked in content + /// + /// Uses `libimagentrymarkdown::processor::LinkProcessor` for this, with the following settings: + /// + /// * Interal link processing = true + /// * Internal targets creating = true + /// * External link processing = true + /// * Processing of Refs = true + /// + /// This is a convenience function for `WikiEntry::autolink_with_processor()`. + /// + /// # Warning + /// + /// With this function, the `LinkProcessor` automatically creates entries in the store if they + /// are linked from the current entry but do not exists yet. + /// + /// # See also + /// + /// * The documentation of `WikiEntry::autolink_with_processor()`. + /// * The documentation of `::libimagentrymarkdown::processor::LinkProcessor`. + /// + fn autolink(&mut self, store: &Store) -> Result<()> { + let processor = LinkProcessor::default() + .process_internal_links(true) + .create_internal_targets(true) + .process_external_links(true) + .process_refs(true); + + self.autolink_with_processor(store, processor) + } + + /// Autolink entry to entries linked in content with the passed `LinkProcessor` instance. + /// + /// See the documentation of `::libimagentrymarkdown::processor::LinkProcessor`. + fn autolink_with_processor(&mut self, store: &Store, processor: LinkProcessor) -> Result<()> { + processor.process(self, store).chain_err(|| WEK::AutoLinkError(self.get_location().clone())) + } + +} diff --git a/lib/domain/libimagwiki/src/error.rs b/lib/domain/libimagwiki/src/error.rs new file mode 100644 index 00000000..185f71f8 --- /dev/null +++ b/lib/domain/libimagwiki/src/error.rs @@ -0,0 +1,55 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2018 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagstore::storeid::StoreId; + +error_chain! { + types { + WikiError, WikiErrorKind, ResultExt, Result; + } + + links { + StoreError(::libimagstore::error::StoreError, ::libimagstore::error::StoreErrorKind); + LinkError(::libimagentrylink::error::LinkError, ::libimagentrylink::error::LinkErrorKind); + } + + errors { + WikiDoesNotExist(name: String) { + description("Wiki does not exist") + display("Wiki '{}' does not exist", name) + } + + WikiExists(name: String) { + description("Wiki exist already") + display("Wiki '{}' exists already", name) + } + + AutoLinkError(sid: StoreId) { + description("Error while autolinking entry") + display("Error while autolinking entry: {}", sid) + } + + MissingIndex { + description("Index page for wiki is missing") + display("Index page for wiki is missing") + } + } + +} + diff --git a/lib/domain/libimagwiki/src/lib.rs b/lib/domain/libimagwiki/src/lib.rs new file mode 100644 index 00000000..ef1801c1 --- /dev/null +++ b/lib/domain/libimagwiki/src/lib.rs @@ -0,0 +1,55 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2018 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +#![recursion_limit="256"] + +#![deny( + dead_code, + non_camel_case_types, + non_snake_case, + path_statements, + trivial_numeric_casts, + unstable_features, + unused_allocation, + unused_import_braces, + unused_imports, + unused_must_use, + unused_mut, + unused_qualifications, + while_true, +)] + +extern crate filters; +extern crate toml; +extern crate toml_query; +#[macro_use] extern crate log; +#[macro_use] extern crate error_chain; + +#[macro_use] extern crate libimagstore; +extern crate libimagerror; +extern crate libimagentrylink; +extern crate libimagentrymarkdown; + +module_entry_path_mod!("wiki"); + +pub mod entry; +pub mod error; +pub mod store; +pub mod wiki; + diff --git a/lib/domain/libimagwiki/src/store.rs b/lib/domain/libimagwiki/src/store.rs new file mode 100644 index 00000000..36106761 --- /dev/null +++ b/lib/domain/libimagwiki/src/store.rs @@ -0,0 +1,92 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2018 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagstore::store::Store; +use libimagstore::storeid::StoreId; +use libimagstore::storeid::IntoStoreId; + +use error::WikiError as WE; +use error::Result; +use wiki::Wiki; + +pub trait WikiStore { + + fn get_wiki<'a, 'b>(&'a self, name: &'b str) -> Result>>; + + fn create_wiki<'a, 'b>(&'a self, name: &'b str, mainpagename: Option<&str>) + -> Result>; + + fn retrieve_wiki<'a, 'b>(&'a self, name: &'b str, mainpagename: Option<&str>) + -> Result>; + +} + +impl WikiStore for Store { + + /// get a wiki by its name + fn get_wiki<'a, 'b>(&'a self, name: &'b str) -> Result>> { + if wiki_path(name.as_ref())?.with_base(self.path().clone()).exists()? { + debug!("Building Wiki object"); + Ok(Some(Wiki::new(self, name))) + } else { + debug!("Cannot build wiki object: Wiki does not exist"); + Ok(None) + } + } + + /// Create a wiki. + /// + /// # Returns + /// + /// Returns the Wiki object. + /// + /// Ob success, an empty Wiki entry with the name `mainpagename` (or "main" if none is passed) + /// is created inside the wiki. + /// + fn create_wiki<'a, 'b>(&'a self, name: &'b str, mainpagename: Option<&str>) + -> Result> + { + debug!("Trying to get wiki '{}'", name); + debug!("Trying to create wiki '{}' with mainpage: '{:?}'", name, mainpagename); + + let wiki = Wiki::new(self, name); + let _ = wiki.create_index_page()?; + + wiki.create_entry(mainpagename.unwrap_or("main")) + .map(|_| wiki) + } + + fn retrieve_wiki<'a, 'b>(&'a self, name: &'b str, mainpagename: Option<&str>) + -> Result> + { + match self.get_wiki(name)? { + None => self.create_wiki(name, mainpagename), + Some(wiki) => { + let _ = wiki.retrieve_entry(mainpagename.unwrap_or("main"))?; // to make sure the page exists + Ok(wiki) + } + } + } + +} + +fn wiki_path(name: &str) -> Result { + ::module_path::ModuleEntryPath::new(name).into_storeid().map_err(WE::from) +} + diff --git a/lib/domain/libimagwiki/src/wiki.rs b/lib/domain/libimagwiki/src/wiki.rs new file mode 100644 index 00000000..50ef61cd --- /dev/null +++ b/lib/domain/libimagwiki/src/wiki.rs @@ -0,0 +1,144 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2018 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::path::PathBuf; + +use filters::filter::Filter; + +use libimagstore::store::Store; +use libimagstore::store::Entry; +use libimagstore::store::FileLockEntry; +use libimagstore::storeid::IntoStoreId; +use libimagstore::storeid::StoreId; +use libimagstore::storeid::StoreIdIteratorWithStore; +use libimagentrylink::internal::InternalLinker; + +use error::WikiError as WE; +use error::WikiErrorKind as WEK; +use error::Result; + +pub struct Wiki<'a, 'b>(&'a Store, &'b str); + +/// An interface for accessing, creating and deleting "wiki pages" +/// +/// Wiki pages are normal entries with some details added. +/// +/// +/// # Details +/// +/// Entries are automatically linked to the "index" page when created and retrieved. +/// +impl<'a, 'b> Wiki<'a, 'b> { + + pub(crate) fn new(store: &'a Store, name: &'b str) -> Wiki<'a, 'b> { + Wiki(store, name) + } + + pub(crate) fn create_index_page(&self) -> Result> { + let path = PathBuf::from(format!("{}/index", self.1)); + let sid = ::module_path::ModuleEntryPath::new(path).into_storeid()?; + + self.0 + .create(sid) + .map_err(WE::from) + } + + pub fn get_entry>(&self, entry_name: EN) -> Result>> { + let path = PathBuf::from(format!("{}/{}", self.1, entry_name.as_ref())); + let sid = ::module_path::ModuleEntryPath::new(path).into_storeid()?; + self.0.get(sid).map_err(WE::from) + } + + pub fn create_entry>(&self, entry_name: EN) -> Result> { + let path = PathBuf::from(format!("{}/{}", self.1, entry_name.as_ref())); + let sid = ::module_path::ModuleEntryPath::new(path).into_storeid()?; + let mut index = self + .get_entry("index")? + .ok_or_else(|| WEK::MissingIndex.into()) + .map_err(WE::from_kind)?; + let mut entry = self.0.create(sid)?; + + entry.add_internal_link(&mut index) + .map_err(WE::from) + .map(|_| entry) + } + + pub fn retrieve_entry>(&self, entry_name: EN) -> Result> { + let path = PathBuf::from(format!("{}/{}", self.1, entry_name.as_ref())); + let sid = ::module_path::ModuleEntryPath::new(path).into_storeid()?; + let mut index = self + .get_entry("index")? + .ok_or_else(|| WEK::MissingIndex.into()) + .map_err(WE::from_kind)?; + let mut entry = self.0.retrieve(sid)?; + + entry.add_internal_link(&mut index) + .map_err(WE::from) + .map(|_| entry) + } + + pub fn all_ids(&self) -> Result { + let filter = IdIsInWikiFilter(self.1); + Ok(WikiIdIterator(self.0.entries()?, filter)) + } + + pub fn delete_entry>(&self, entry_name: EN) -> Result<()> { + let path = PathBuf::from(format!("{}/{}", self.1, entry_name.as_ref())); + let sid = ::module_path::ModuleEntryPath::new(path).into_storeid()?; + self.0.delete(sid).map_err(WE::from) + } +} + +pub struct WikiIdIterator<'a>(StoreIdIteratorWithStore<'a>, IdIsInWikiFilter<'a>); + +impl<'a> Iterator for WikiIdIterator<'a> { + type Item = StoreId; + + fn next(&mut self) -> Option { + while let Some(next) = self.0.next() { + if self.1.filter(&next) { + return Some(next) + } + } + + None + } +} + +pub struct IdIsInWikiFilter<'a>(&'a str); + +impl<'a> IdIsInWikiFilter<'a> { + pub fn new(wiki_name: &'a str) -> Self { + IdIsInWikiFilter(wiki_name) + } +} + +impl<'a> Filter for IdIsInWikiFilter<'a> { + fn filter(&self, id: &StoreId) -> bool { + id.is_in_collection(&["wiki", &self.0]) + } +} + +impl<'a> Filter for IdIsInWikiFilter<'a> { + fn filter(&self, e: &Entry) -> bool { + e.get_location().is_in_collection(&["wiki", &self.0]) + } +} + + diff --git a/scripts/release.sh b/scripts/release.sh index 00969827..f1b8520e 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -30,6 +30,7 @@ CRATES=( ./lib/domain/libimagtimetrack ./lib/domain/libimagtodo ./lib/domain/libimagmail + ./lib/domain/libimagwiki ./bin/domain/imag-habit ./bin/domain/imag-diary ./bin/domain/imag-contact