use crate::bookmark::{Bookmark, Handler, HandlerClass, Info, Location, LocationType};
use crate::data_provider::{DataProvider, DomainCode, Format, Handlers, Settings, SpecialPages};
use crate::Scope;
use config_file::FromConfigFile;
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::path::Path;
#[derive(Clone)]
pub struct Category {
pub name: String,
pub options: Configurations,
}
impl Category {
pub fn new(scope: Scope) -> Self {
let name = scope.get_name();
let options = Configurations::new(scope);
Self { name, options }
}
pub fn entries(&self) -> HashSet<Bookmark> {
let mut entries: HashSet<Bookmark> = HashSet::new();
let bookmarks = self.options.get_bookmarks();
bookmarks.into_iter().for_each(|bookmark| {
entries.insert(bookmark);
});
entries
}
}
#[derive(Clone)]
pub struct Configurations {
scope: Scope,
}
impl Configurations {
fn new(scope: Scope) -> Self {
Self { scope }
}
pub fn script_dir() -> String {
if Self::get_setting().script_dir.is_some() {
let script = Self::get_setting().script_dir.unwrap();
if !script.contains('/') {
format!("{}/{script}", Self::get_app_home())
} else {
script
}
} else {
format!("{}/scripts", Self::get_app_home())
}
}
fn help_file() -> String {
format!("{}/docs/guide.html", Self::get_app_home())
}
pub fn config_file() -> String {
format!("{}/config.toml", Self::get_app_home())
}
pub fn get_records() -> String {
format!("{}/records", Self::get_app_home())
}
pub fn record(record: String) {
fs::write(format!("{}/last", Self::get_records()), record.as_bytes())
.expect("Could not record just-visited bookmark");
}
pub fn get_last_visit() -> String {
let fname = format!("{}/last", Self::get_records());
if Self::check_path(&fname) {
fs::read_to_string(fname).unwrap()
} else {
eprintln!(
"Hi! No record of your last visit was found on this machine. Perhaps this is your first run of this program?"
);
eprintln!(
"This record can only be generated when you have at least visited one bookmark with this program. Bye!"
);
std::process::exit(38);
}
}
pub fn category_home() -> String {
format!("{}/bookmarks", Self::get_app_home())
}
pub fn get_homepage() -> String {
let special_pages = Self::get_setting().special_pages;
if special_pages.is_some() {
let home = special_pages.unwrap().home;
if home.is_some() {
let set_home = home.unwrap().clone();
if set_home == "." {
format!(
"{}.CATEGORY_HOME",
Self::get_setting()
.default_category
.unwrap_or(Self::default_settings().default_category.unwrap())
)
} else if set_home.ends_with('.') {
format!("{set_home}CATEGORY_HOME")
} else if set_home.starts_with('.') {
format!(
"{}{set_home}",
Self::get_setting()
.default_category
.unwrap_or(Self::default_settings().default_category.unwrap())
)
} else {
set_home
}
} else {
format!(
"{}.CATEGORY_HOME",
Self::get_setting()
.default_category
.unwrap_or(Self::default_settings().default_category.unwrap())
)
}
} else {
format!(
"{}.CATEGORY_HOME",
Self::get_setting()
.default_category
.unwrap_or(Self::default_settings().default_category.unwrap())
)
}
}
pub fn get_setting() -> Settings {
if !Self::check_path(&Self::config_file()) {
Self::create_sample_data(&Self::config_file(), DataProvider::new("config").data())
.expect("Could not create default configuration file");
}
FromConfigFile::from_config_file(Self::config_file()).unwrap()
}
pub fn default_settings() -> Settings {
let pages = SpecialPages {
home: Some("HLG_HELP".to_string()),
search: Some("HLG_SEARCH".to_string()),
wiki: Some("HLG_WIKI".to_string()),
};
let handlers = Handlers {
browser: Some("firefox".to_string()),
file_manager: Some("caja".to_string()),
command: Some("bash".to_string()),
};
let domain_codes = vec![
DomainCode {
code: 'b',
value: "blog".to_string(),
},
DomainCode {
code: 'w',
value: "wiki".to_string(),
},
];
let formats = vec![
Format {
name: "vim".to_string(),
extensions: vec![
"txt".to_string(),
"c".to_string(),
"conf".to_string(),
"cfg".to_string(),
],
},
Format {
name: "libreoffice".to_string(),
extensions: vec![
"odf".to_string(),
"odt".to_string(),
"docx".to_string(),
"doc".to_string(),
"ppt".to_string(),
"pptx".to_string(),
"xls".to_string(),
"xlsx".to_string(),
],
},
];
Settings {
default_category: Some("default".to_string()),
create_missing_category: Some(false),
autoselect: Some(true),
save_with_shortcuts: Some(true),
script_dir: Some("scripts".to_string()),
special_pages: Some(pages),
handlers: Some(handlers),
domain_codes: Some(domain_codes),
formats: Some(formats),
}
}
pub fn initialize_defaults() -> std::io::Result<()> {
let app_dir = Self::get_app_home();
if !Self::check_path(&app_dir) {
Self::create_app_dir(app_dir);
}
let record_dir = Self::get_records();
if !Self::check_path(&record_dir) {
fs::create_dir(&record_dir)?;
}
if !Self::check_path(&Self::category_home()) {
fs::create_dir(&Self::category_home())?;
}
if !Self::check_path(&Self::help_file()) {
let docs_dir = format!("{}/docs", Self::get_app_home());
if !Self::check_path(&docs_dir) {
fs::create_dir(&docs_dir)?;
}
let help_data = DataProvider::new("help").data();
Self::create_sample_data(&Self::help_file(), help_data)?;
}
if !Self::check_path(&Self::script_dir()) {
fs::create_dir(&Self::script_dir())?;
}
let bookmark_file = format!("{}/default", Self::category_home());
if !Self::check_path(&bookmark_file) {
let default_bookmarks = DataProvider::new("bookmarks").data();
Self::create_sample_data(&bookmark_file, default_bookmarks)?;
}
if !Self::check_path(&Self::config_file()) {
Self::create_sample_data(&Self::config_file(), DataProvider::new("config").data())?;
}
Ok(())
}
fn create_sample_data(file: &str, sample: Vec<u8>) -> std::io::Result<()> {
fs::write(file, sample)?;
Ok(())
}
fn builtin_bookmarks() -> Vec<Bookmark> {
let path = LocationType::new("@en.wikipedia.org/wiki".to_string());
let wiki = Bookmark::new(
Info::new("HLG_WIKI+"),
Location {
handler: Handler::new().name_from_protocol(HandlerClass::new(&path)),
has_variable: false,
path,
},
);
let search_path = LocationType::new("@google.com".to_string());
let search = Bookmark::new(
Info::new("HLG_SEARCH?"),
Location {
handler: Handler::new().name_from_protocol(HandlerClass::new(&search_path)),
has_variable: false,
path: search_path,
},
);
let builtins = vec![Bookmark::default(), search, wiki];
builtins
}
fn get_bookmarks(&self) -> Vec<Bookmark> {
let mut bookmarks: Vec<Bookmark> = Vec::new();
bookmarks.extend(Self::builtin_bookmarks());
let rawtext =
Self::load_text_file(self.scope.clone()).expect("Could not load bookmarks from file");
let mut names = Vec::new();
for (raw_name, raw_address, raw_program) in rawtext {
let info = Info::new(&raw_name);
if self.scope.is_local() && info.is_global {
continue;
}
if !self.scope.is_local() && !info.is_global {
continue;
}
let name = info.name.clone();
if !names.contains(&name) {
names.push(name);
} else {
if name == "CATEGORY_HOME" {
eprintln!(
"Error: you cannot have more than one category home bookmarks in the same scope as is the case with the '{}' category.",
self.scope.get_name()
);
eprintln!(
"Please fix this error by deleting other entries with the asterisk symbol in the bookmark name field, and retain only one to serve as the home bookmark for the {} category.",
self.scope.get_name()
);
} else {
eprintln!("Error: name duplicates detected!");
eprintln!(
"This name, '{name}' is assigned to more than one bookmark entry in the same category, '{}'",
self.scope.get_name()
);
eprintln!(
"Please fix this error by simply deleting other bookmark entries with the name '{name}' and keep only one entry."
);
}
std::process::exit(20);
}
let address = &raw_address;
let has_variable = address.starts_with("@") && address.contains("{");
let path = LocationType::new(String::from(address));
let app = &raw_program;
let handler = if app.contains(":hapana:") && !has_variable {
let program = Handler::new().name_from_protocol(HandlerClass::new(&path));
if address.starts_with(":!") {
program.options("-c".to_string())
} else {
program
}
} else if app.starts_with('(') {
let command = Self::extract_arguments(&app[1..], &info);
Handler::new()
.name_from_protocol(HandlerClass::new(&path))
.options(command.to_string())
} else {
if app.contains('(') {
let pos = app.find('(').unwrap();
let (program, cmd) = app.split_at(pos);
let arguments = Self::extract_arguments(&cmd[1..], &info);
Handler::new()
.name(program.to_string())
.options(arguments.to_string())
} else {
Handler::new().name(app.to_string())
}
};
let location = Location {
path,
handler,
has_variable,
};
bookmarks.push(Bookmark::new(info, location));
}
names.clear();
Self::check_key_collision(&bookmarks);
bookmarks
}
fn extract_arguments<'a>(command: &'a str, bookmark_name: &Info) -> &'a str {
if !command.ends_with(')') {
eprintln!(
"Error: The command block for the {} bookmark not properly closed. Please close this block for your commands to be processed properly.",
bookmark_name.name
);
std::process::exit(19);
}
let argument = command.strip_suffix(')').unwrap();
let argument = argument.trim();
if argument.is_empty() {
eprintln!(
"Error: The command-line block for the {} bookmark is empty. Please remove the block completely or else supply the commands to the {} handler to fix this error.",
bookmark_name.name, bookmark_name.name
);
std::process::exit(19);
}
argument
}
fn check_key_collision(bookmarks: &Vec<Bookmark>) {
let mut keys = Vec::new();
for bookmark in bookmarks {
if bookmark.info.key.is_some() {
keys.push(bookmark.info.get_key());
}
}
for key in keys {
let mut names = Vec::new();
for bookmark in bookmarks {
if bookmark.info.key.is_some() {
if key == bookmark.info.get_key() {
names.push(bookmark.info.name.clone());
}
}
}
if names.len() > 1 {
eprintln!("Error: bookmark shortcut key duplication detected!");
eprintln!(
"The key '{key}' is assigned to {} bookmarks, namely: {} and {}",
names.len(),
&names[..names.len() - 1].join(", "),
&names[names.len() - 1]
);
eprintln!(
"To fix this error, remove this key assignment '{key}' on all other bookmarks and only assign it to one bookmark."
);
std::process::exit(19);
}
}
}
fn load_text_file(scope: Scope) -> std::io::Result<Vec<(String, String, String)>> {
let entry_file = match scope {
Scope::Local(_) => {
let filename = format!("{}/{}", Self::category_home(), scope.get_name());
fs::read_to_string(&filename)?
}
Scope::Global => {
let mut all_files = String::new();
let files = fs::read_dir(&Self::category_home())
.unwrap()
.into_iter()
.filter_map(Result::ok)
.filter(|f| f.file_name() != ".last");
for catfile in files {
let filename = format!("{}", catfile.path().display());
all_files.push_str(&fs::read_to_string(&filename).unwrap());
}
all_files
}
};
let entry_file: Vec<&str> = entry_file
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect();
let comment_list = entry_file
.iter()
.enumerate()
.filter(|(_, line)| !line.starts_with("#!"))
.filter(|(_index, item)| item.starts_with("#"))
.collect::<HashMap<_, _>>();
let reserved_names = vec!["CATEGORY_HOME", "HLG_SEARCH", "HLG_WIKI", "HLG_HELP"];
let mut raw_materials = Vec::new();
let header = entry_file.get(0).unwrap();
if header.starts_with("#!") && scope.get_name() != "<global>" {
let fields: Vec<&str> = header.split(';').collect();
let home_bookmark = fields.get(0).unwrap();
let home_bookmark = &home_bookmark[2..];
if home_bookmark.contains('<') {
let home_bookmark = home_bookmark.trim();
raw_materials.push(Self::prepare_home_bookmark(
home_bookmark,
&scope.get_name(),
));
}
}
for (position, candidate) in entry_file.iter().enumerate() {
if candidate.starts_with("#") {
continue;
}
let start = candidate.find('<');
let end = candidate.find('>');
let start = if start.is_some() {
start.unwrap()
} else {
eprintln!("Error, could not find location field in this entry: '{candidate}'");
eprintln!(
"A location field is denoted by the location enclosed in angle brackets, <location>. Please fix this error and try again."
);
std::process::exit(19);
};
let end = if end.is_some() {
end.unwrap() + 1
} else {
eprintln!(
"Error, no closing angle bracket, >, was found for this entry: '{candidate}'. "
);
eprintln!("Please fix this error and try again.");
std::process::exit(19);
};
let first: String = candidate.chars().take(start).collect();
let second: String = candidate.chars().skip(start).take(end - start).collect();
let third: String = candidate.chars().skip(end).collect();
let refname: &str;
let first = {
let name = first.trim();
if !name.is_empty() {
if name.contains("#") {
eprintln!(
"Error: you have an invalid character '#' in your bookmark name {name}"
);
eprintln!("Please remove it to fix this error.");
std::process::exit(19);
}
if name.len() == 1 {
eprintln!(
"Error: a bookmark name must be at least two characters in length."
);
eprintln!(
"A single character is reserved for bookmark shortcuts. So fix the error by assigning a valid name on this bookmark, '{name}' and try again."
);
std::process::exit(19);
}
reserved_names.iter().for_each(|r_name| if name.contains(r_name) {
let reason =match *r_name {
"CATEGORY_HOME" => "it is used internally for home bookmark detection and organization",
"HLG_SEARCH" => "it is used for the builtin Google search bookmark",
"HLG_HELP" => "it is used for the builtin quick guide",
"HLG_WIKI" => "it is used for the builtin wiki bookmark that links to Wikipedia",
&_ => todo!(),
};
eprintln!("Error: use of reserved name, '{r_name}' in a bookmark name.");
eprintln!("You cannot use {r_name} in a bookmark name because {reason}.");
eprintln!("To fix this error, just rename {name} to some other name.");
std::process::exit(60);
} );
refname = name;
let target = if position > 0 { position - 1 } else { 0 };
if comment_list.contains_key(&target) {
let tag = comment_list.get(&target).unwrap();
format!("{name}{tag}")
} else {
name.to_string()
}
} else {
eprintln!("Error: no bookmark name was found for this entry: '{candidate}'.");
eprintln!(
"Please fix this error by providing a name and run this program again."
);
std::process::exit(19);
}
};
let second = {
let address = second.replace("<", "").replace(">", "");
let address = address.trim();
if !address.is_empty() {
address.to_string()
} else {
eprintln!("Error: this entry '{candidate}' has an empty location field.");
eprintln!(
"Please fix this by adding the destination for this bookmark in the angle brackets."
);
std::process::exit(19);
}
};
let second = if second.starts_with('@') && second.contains('*') && !second.contains('{')
{
if refname.ends_with(':') {
let home = Self::get_homepage();
second.replace("*", &format!("{{{}}}/", home))
} else {
second.replace("*", "{CATEGORY_HOME}/")
}
} else {
second
};
if second.starts_with("@www.") {
println!(
"Warning: this link shortcut on the {refname} bookmark reference can be improved."
);
println!(
"Instead of the 'www.' string, consider using the '=' qualifier so that instead of '@www.' you simply write it as '@='"
);
}
if second.starts_with('@') && second.contains('{') {
Self::do_reference_check(&second, refname, scope.get_name());
}
let third = {
let app = third.trim();
let app = if app.is_empty() || app.len() == 1 {
"::hapana:"
} else {
if !app.starts_with(':') {
eprintln!(
"Error: this entry {candidate}'s handler field is malformed. A handler field must always start with a colon (:) character, then the name of the application to run this bookmark. It could be a full path or just a name as it is shown in the $PATH environment variable."
);
eprintln!(
"Please fix this error by just prepending a colon to {app} and run this program again."
);
std::process::exit(19);
}
app
};
app.strip_prefix(':').unwrap().to_string()
};
raw_materials.push((first, second, third));
}
Ok(raw_materials)
}
pub fn check_header_bookmark(home: &str, fname: &str) {
if home.is_empty() {
eprintln!("Error: Your category header field for the category {fname} is invalid.");
eprintln!(
"Once you add a header line with the '#!' symbol, you have to add something to one of the three fields. This bookmark field has nothing in it."
);
std::process::exit(19);
}
if !home.contains('<') {
eprintln!(
"Error: The home bookmark of the category {fname} category has no destination in it."
);
eprintln!("Please add one in the location container and run this program again.");
std::process::exit(19);
}
if !home.ends_with('>') {
eprintln!(
"Error: the location container of the {fname} category home bookmark is not properly terminated."
);
eprintln!("Please add a closing '>' symbol to close it.");
std::process::exit(19);
}
}
pub fn check_header_key(token: &str, fname: &str) {
if token.len() > 1 {
eprintln!("Error: invalid characters in the {fname}'s category header.");
eprintln!(
"Only enter one character in the shortcut key field. This character is going to be the shortcut key for the category {fname}"
);
eprintln!(
"If You do not want to set a shortcut key for this category, just add a semicolon after the bookmark field to ignore the shortcut key."
);
std::process::exit(19);
}
let token = token.chars().nth(0).unwrap();
if !token.is_alphanumeric() {
eprintln!("Error: only use alphanumeric characters as category shortcuts.");
eprintln!(
"This token '{token}' in the '{fname}' category header is an invalid character as it cannot be used as a shortcut key."
);
std::process::exit(19);
}
}
fn prepare_home_bookmark(home_field: &str, category: &String) -> (String, String, String) {
Self::check_header_bookmark(home_field, &category);
let (handler, address) = home_field.split_at(home_field.find('<').unwrap());
if address.contains(' ') {
eprintln!(
"Error: A location container of the category header is always a linking location so it cannot have embedded spaces."
);
eprintln!(
"Please fix this error by removing all the spaces in the {category} category header bookmark field."
);
std::process::exit(19);
}
if address.starts_with("<@*") {
eprintln!(
"Error: The bookmark field of the {category} category header is trying to link to itself."
);
eprintln!(
"It is already the home bookmark for the {category} category which means all the bookmarks in this category can link to it using this '@*' construction."
);
std::process::exit(19);
}
if address.starts_with("<:") {
eprintln!("Error: You cannot have a command location in the category home bookmark.");
eprintln!(
"To fix this error, remove the ':' prefix in the bookmark field of the {category} category header."
);
std::process::exit(19);
}
let name = "CATEGORY_HOME".to_string();
let address = format!("{}", &address[1..].strip_suffix('>').unwrap());
if address.trim().is_empty() {
eprintln!(
"Error: The home bookmark of the {category} category has no destination in it."
);
eprintln!(
"Please fix this error by supplying the resource, whether on the file system or to a remote location inside the angle brackets and try again."
);
std::process::exit(19);
}
let handler = if handler.trim().is_empty() {
"::hapana:".to_string()
} else {
handler.trim().to_string()
};
(name, address, handler)
}
fn do_reference_check(second: &String, refname: &str, reference_category: String) {
if second.find('}').is_none() {
eprintln!("Error: This entry, '{refname}' is not properly closed.");
eprintln!(
"Please add a closing curly bracket if you want to turn it into a reference."
);
std::process::exit(19);
}
let (domain, _) = second.split_at(second.find('{').unwrap());
let start = second.find('{').unwrap() + 1;
let end = second.find('}').unwrap();
let target: String = second.chars().skip(start).take(end - start).collect();
let target = target.trim();
if target.is_empty() || target.contains(" ") {
eprintln!("Error: This entry, '{refname}' has an invalid target {target}.");
eprintln!("Remember spaces are not allowed inside references or links.");
eprintln!(
"Please fix this error by providing a valid target and removing all spaces in the location field for {refname}."
);
std::process::exit(19);
}
if refname.ends_with(':') && target.starts_with('/') {
let target = target.strip_prefix('/').unwrap();
eprintln!(
"Error: both this reference bookmark '{refname}' and its target bookmark, '{target}', are global in scope."
);
eprintln!(
"This means that there is no need for the leading forward slash symbol on the target. Just use a bare name like this, '{target}' as they are in the same scope."
);
std::process::exit(19);
}
if (target.starts_with('/') || target.starts_with('*')) && target.contains('.') {
eprintln!("Error: this bookmark '{refname}' has an ambiguous target.");
eprintln!(
"A reference target can either be in the global scope or in another category scope, but not both. Fix this error by either removing the forward slash which is a global scope indicator, or the period in the target."
);
eprintln!(
"Using a forward slash in a target means you are targeting the global scope, while an asterisk is targeting the category home bookmark of another scope."
);
eprintln!(
"Such references have already provided the scope, and are not allowed to further state another scope with a period as part of a fully-qualified target bookmark name."
);
std::process::exit(19);
}
if target.starts_with('*') {
let target_category = target.strip_prefix('*').unwrap();
if reference_category == target_category && !refname.ends_with(':') {
eprintln!(
"Error: use of a fully-qualified target name by a local reference to a local target, {target}."
);
eprintln!(
"To target a local category home bookmark, just use the '@*' notation and save yourself keystrokes in the process."
);
std::process::exit(19);
}
}
let target = if target.starts_with('/') || target.starts_with('*') {
&target[1..]
} else {
target
};
if target.trim().is_empty() {
eprintln!("Error: This reference bookmark, {refname}, has an empty target.");
eprintln!("Please supply a valid target to fix this error.");
std::process::exit(19);
}
let b = target.chars().nth(0).unwrap();
let e = target.chars().nth(target.len() - 1).unwrap();
if !b.is_alphanumeric() || !e.is_alphanumeric() {
eprintln!(
"Error: This reference bookmark, {refname}, has illegal characters in its target {target}."
);
eprintln!(
"Please make sure you supply valid targets in the location field of a reference bookmark."
);
eprintln!("Only alphanumeric characters can start or end a target name.");
std::process::exit(19);
}
if target.contains(".") {
let pos = target.rfind('.').unwrap();
let (category, handle) = target.split_at(pos);
if category.contains(".") {
eprintln!(
"Error: this category being targeted to by the {refname} reference is invalid."
);
eprintln!("As no category is allowed to have periods, so this target is invalid.");
std::process::exit(19);
}
if reference_category == category {
let handle = handle.strip_prefix('.').unwrap();
if !refname.ends_with(':') {
eprintln!(
"Error: both the reference bookmark '{refname}' and its target '{handle}' are in the same category '{category}'', so they share a local scope."
);
eprintln!(
"So just use the bare name '{handle}' rather than its fully-qualified name."
);
std::process::exit(19);
}
}
}
let (_, outside) = second.split_at(end);
let outside = outside.strip_prefix('}').unwrap();
let domain = domain.strip_prefix('@').unwrap();
let domain = domain.trim();
let outside = outside.trim();
if domain.is_empty() && (outside.is_empty() || outside.len() == 1) {
eprintln!(
"Error: no dead references are permitted in hlg. Currently, {refname} is a dead reference as it adds no extra information to {target}"
);
eprintln!(
"References only add value to targets if they have a directory or a subdomain to use on the target. As the target is a convenient way of locating a resource, you have to use it as a shortcut."
);
eprintln!(
"Instead of {refname}, consider using {target} directly to avoid dead references."
);
std::process::exit(19);
}
}
fn create_app_dir(dir: String) {
fs::create_dir_all(dir)
.expect("Could not create the app directory. Check permissions and try again!");
}
pub fn check_path(path: &str) -> bool {
Path::new(path).exists()
}
pub fn get_app_home() -> String {
let my_home = env::var("HLG_HOME").unwrap_or("$HOME/.hlg".to_string());
format!("{}", shellexpand::full(&my_home).unwrap())
}
}
pub struct LinkVariable {
dictionary: HashSet<Bookmark>,
template: String,
is_a_domain: bool,
replace_w: bool,
category: String,
bookmark_name: String,
}
impl LinkVariable {
pub fn new(template: String, category: Category, bookmark_name: String) -> Self {
let dictionary = category.entries();
let is_a_domain = Self::has_sub(template.strip_prefix('@').unwrap());
let replace_w = template.contains("!{") && is_a_domain;
if template.contains("!{") && !replace_w {
eprintln!("Error: Invalid token placed in a link shortcut.");
eprintln!(
"An exclamation mark placed here in {template} means to substitute a 'www' subdomain for a given key or string."
);
eprintln!(
"Please, remove the exclamation mark to fix this error, or simply provide a valid key or string."
);
std::process::exit(58);
}
let category = category.name.clone();
Self {
template,
dictionary,
is_a_domain,
replace_w,
category,
bookmark_name,
}
}
fn get_variable(&mut self) -> String {
let start = self.template.find('{').unwrap() + 1;
let end = self.template.find('}').unwrap();
self.template
.chars()
.skip(start)
.take(end - start)
.collect()
}
pub fn expand(mut self) -> String {
let meaning = self.value_of();
let mytemplate = self.template.clone();
let mytemplate = mytemplate
.replace(&self.get_variable(), &meaning)
.replace("}", "");
if self.is_a_domain {
self.process_subdomain(mytemplate)
} else {
mytemplate.replace("@{", "")
}
}
fn second_level_processing(&self, bookmark: &Bookmark) -> String {
let template = bookmark.location.get();
let mut new_cat = self.category.clone();
let key: &str;
let mut dictionary = HashSet::new();
let (target, _) = template.split_at(template.find('}').unwrap());
let target = &target[2..];
if target.starts_with('/') {
new_cat = "<global>".to_string();
key = target.strip_prefix('/').unwrap();
dictionary.extend(Scope::extend_with("<global>"));
} else if target.contains('.') {
let (category, handle) = target.split_at(target.find('.').unwrap());
let handle = handle.strip_prefix('.').unwrap();
if category == new_cat && self.bookmark_name == handle {
eprintln!("Danger: self-recursive call chain detected!");
eprintln!(
"This bookmark target {handle} is referring to itself from another level of the target call chain."
);
eprintln!("Please check your targets to fix this error.");
std::process::exit(13);
}
new_cat = category.to_string();
key = handle;
dictionary.extend(Scope::extend_with(category));
} else {
key = target;
dictionary = self.dictionary.clone();
}
let bookmark = dictionary.get(key);
if bookmark.is_some() {
if bookmark.unwrap().location.has_variable {
eprintln!("Error: third level recursion not allowed.");
eprintln!(
"You can only make two levels of recursive calls from the current scope, {}.",
self.category
);
std::process::exit(55);
}
template
.replace(key, &bookmark.unwrap().location.get())
.replace("*", "")
} else {
eprintln!(
"Error: target bookmark, '{key}' could not be found in the '{new_cat}' category."
);
eprintln!("Please check spelling, case or scope for this target {key} and try again.");
std::process::exit(35);
}
}
fn value_of(&mut self) -> String {
let mut recursion_chain = vec![];
let work_with = self.get_variable();
let qlfbookmark = if !work_with.contains('.') {
format!("{}.{work_with}", self.category)
} else {
work_with.clone()
};
if !recursion_chain.contains(&qlfbookmark) {
recursion_chain.push(qlfbookmark);
}
let ref_category: String;
let work_with = {
if work_with.starts_with('/') {
ref_category = "global scope".to_string();
self.dictionary.clear();
self.dictionary.extend(Scope::extend_with("<global>"));
work_with.strip_prefix('/').unwrap().to_string()
} else if work_with.starts_with('*') {
ref_category = format!("{} category", work_with.strip_prefix('*').unwrap());
let category = work_with.strip_prefix('*').unwrap();
self.dictionary.clear();
self.dictionary.extend(Scope::extend_with(category));
"CATEGORY_HOME".to_string()
} else if work_with.contains(".") {
let pos = work_with.find('.').unwrap();
let (category, variable) = work_with.split_at(pos);
let variable = variable.strip_prefix('.').unwrap();
if !recursion_chain.contains(&work_with) {
recursion_chain.push(work_with.clone());
}
ref_category = format!("{category} category");
if self.category != category {
self.dictionary.extend(Scope::extend_with(category));
}
variable.to_string()
} else {
ref_category = format!("current {} category", self.category);
work_with
}
};
let bookmark = self.dictionary.get(work_with.as_str());
if let Some(thing) = bookmark {
if thing.location.is_a_command() {
eprintln!(
"Error: Illegal target to the command bookmark '{thing}' by the {} reference bookmark, or some other target along its call chain.",
self.bookmark_name
);
eprintln!(
"A reference cannot target a command, and neither does a command allowed to contain a reference."
);
std::process::exit(12);
}
if self.is_a_domain {
let mut target = thing.location.get();
if !target.starts_with("http") {
eprintln!(
"Error: One or more reference bookmarks are creating subdomain references to this target bookmark, {thing}"
);
eprintln!(
"You can only create subdomain references that point to URLs and not any other resource."
);
eprintln!(
"As {target} is not a web Url, please fix this error by removing any subdomain reference pointing to it."
);
std::process::exit(55);
}
target = if self.replace_w {
target.replace("www.", "")
} else {
target
};
target.replace("https://", "").replace("http://", "")
} else if thing.location.has_variable && !self.is_a_domain {
self.second_level_processing(thing)
} else {
thing.location.get()
}
} else {
eprintln!(
"Error, this name '{work_with}' is not resolving to a valid target in the {ref_category} {}.",
self.category
);
eprintln!(
"Please check spelling, case or perhaps in the other categories for {work_with} rather than this scope."
);
std::process::exit(11);
}
}
fn swap_www(&self) -> &str {
let stop_point = self.template.find("!{").unwrap();
let (newdomain, _) = self.template.split_at(stop_point);
newdomain.strip_prefix('@').unwrap()
}
fn inflate_link(&self, letter: &str) -> String {
let mut inflated = letter.to_string();
let symbol = letter.chars().nth(0).unwrap();
let codes = Configurations::get_setting()
.domain_codes
.unwrap_or(Configurations::default_settings().domain_codes.unwrap())
.clone();
for code in codes {
if symbol == code.code {
inflated = code.value.clone();
break;
}
}
inflated
}
fn has_sub(template: &str) -> bool {
let (sub, _) = template.split_at(template.find('{').unwrap());
!sub.is_empty()
}
fn process_subdomain(&self, mut candidate: String) -> String {
candidate = format!("{}", candidate.strip_prefix('@').unwrap());
let (domain, host) = candidate.split_at(candidate.find('{').unwrap());
let domain = if self.replace_w {
self.swap_www()
} else {
domain
};
let domain = if domain.len() == 1 {
self.inflate_link(domain)
} else {
domain.to_string()
};
let host = host.strip_prefix('{').unwrap();
let site = format!("{domain}.{host}");
Self::shortcut_processor(&site)
}
pub fn shortcut_processor(destination: &str) -> String {
let url = "http://";
let surl = "https://";
let token = destination.chars().nth(0).unwrap();
let domain = if token == '!' || token == '=' || token == '+' || token == '#' || token == '~'
{
destination.strip_prefix(token).unwrap()
} else {
destination
};
match token {
'!' => {
let domain = if domain.starts_with('=') {
format!("www.{}", domain.strip_prefix('=').unwrap())
} else {
domain.to_string()
};
format!("{url}{domain}")
}
'=' => format!("{surl}www.{domain}"),
'+' => format!("file://{domain}"),
'~' => format!("{}{domain}", shellexpand::full("file://$HOME/").unwrap()),
'#' => format!("{url}localhost:{domain}"),
_ => format!("{surl}{domain}"),
}
}
}