diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs
index 0ccee49..01f648b 100644
--- a/src/compiler/mod.rs
+++ b/src/compiler/mod.rs
@@ -1,3 +1,4 @@
 pub mod compiler;
 pub mod navigation;
+pub mod process;
 pub mod postprocess;
diff --git a/src/compiler/postprocess.rs b/src/compiler/postprocess.rs
index 85f3263..b8a50be 100644
--- a/src/compiler/postprocess.rs
+++ b/src/compiler/postprocess.rs
@@ -19,7 +19,7 @@ impl PostProcess {
 	/// Applies postprocessing to a [`CompiledDocument`]
 	pub fn apply(
 		&self,
-		target: Target,
+		_target: Target,
 		list: &Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>,
 		doc: &RefCell<CompiledDocument>,
 	) -> Result<String, String> {
diff --git a/src/compiler/process.rs b/src/compiler/process.rs
new file mode 100644
index 0000000..5c86708
--- /dev/null
+++ b/src/compiler/process.rs
@@ -0,0 +1,195 @@
+use std::cell::RefCell;
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::time::UNIX_EPOCH;
+
+use rusqlite::Connection;
+
+use crate::document::document::Document;
+use crate::parser::langparser::LangParser;
+use crate::parser::parser::Parser;
+use crate::parser::parser::ParserState;
+use crate::parser::source::Source;
+use crate::parser::source::SourceFile;
+
+use super::compiler::CompiledDocument;
+use super::compiler::Compiler;
+use super::compiler::Target;
+use super::postprocess::PostProcess;
+
+/// Parses a source file into a document
+fn parse(
+	parser: &LangParser,
+	source: Rc<dyn Source>,
+	debug_opts: &Vec<String>,
+) -> Result<Box<dyn Document<'static>>, String> {
+	// Parse
+	//let source = SourceFile::new(input.to_string(), None).unwrap();
+	let (doc, _) = parser.parse(ParserState::new(parser, None), source.clone(), None);
+
+	if debug_opts.contains(&"ast".to_string()) {
+		println!("-- BEGIN AST DEBUGGING --");
+		doc.content()
+			.borrow()
+			.iter()
+			.for_each(|elem| println!("{elem:#?}"));
+		println!("-- END AST DEBUGGING --");
+	}
+	if debug_opts.contains(&"ref".to_string()) {
+		println!("-- BEGIN REFERENCES DEBUGGING --");
+		let sc = doc.scope().borrow();
+		sc.referenceable.iter().for_each(|(name, reference)| {
+			println!(" - {name}: `{:#?}`", doc.get_from_reference(reference));
+		});
+		println!("-- END REFERENCES DEBUGGING --");
+	}
+	if debug_opts.contains(&"var".to_string()) {
+		println!("-- BEGIN VARIABLES DEBUGGING --");
+		let sc = doc.scope().borrow();
+		sc.variables.iter().for_each(|(_name, var)| {
+			println!(" - `{:#?}`", var);
+		});
+		println!("-- END VARIABLES DEBUGGING --");
+	}
+
+	if parser.has_error() {
+		return Err("Parsing failed due to errors while parsing".to_string());
+	}
+
+	Ok(doc)
+}
+
+/// Takes a list of paths and processes it into a list of compiled documents
+pub fn process(
+	target: Target,
+	files: Vec<PathBuf>,
+	db_path: &Option<String>,
+	force_rebuild: bool,
+	debug_opts: &Vec<String>,
+) -> Result<Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>, String> {
+	let mut compiled = vec![];
+
+	let current_dir = std::env::current_dir()
+		.map_err(|err| format!("Unable to get the current working directory: {err}"))?;
+
+	let con = db_path
+		.as_ref()
+		.map_or(Connection::open_in_memory(), |path| Connection::open(path))
+		.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
+	CompiledDocument::init_cache(&con)
+		.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
+
+	let parser = LangParser::default();
+	for file in files {
+		let meta = std::fs::metadata(&file)
+			.map_err(|err| format!("Failed to get metadata for `{file:#?}`: {err}"))?;
+
+		let modified = meta
+			.modified()
+			.map_err(|err| format!("Unable to query modification time for `{file:#?}`: {err}"))?;
+
+		// Move to file's directory
+		let file_parent_path = file
+			.parent()
+			.ok_or(format!("Failed to get parent path for `{file:#?}`"))?;
+		std::env::set_current_dir(file_parent_path)
+			.map_err(|err| format!("Failed to move to path `{file_parent_path:#?}`: {err}"))?;
+
+		let parse_and_compile = || -> Result<(CompiledDocument, Option<PostProcess>), String> {
+			// Parse
+			let source = SourceFile::new(file.to_str().unwrap().to_string(), None).unwrap();
+			println!("Parsing {}...", source.name());
+			let doc = parse(&parser, Rc::new(source), debug_opts)?;
+
+			// Compile
+			let compiler = Compiler::new(target, db_path.clone());
+			let (mut compiled, postprocess) = compiler.compile(&*doc);
+
+			compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
+
+			Ok((compiled, Some(postprocess)))
+		};
+
+		let (cdoc, post) = if force_rebuild {
+			parse_and_compile()?
+		} else {
+			match CompiledDocument::from_cache(&con, file.to_str().unwrap()) {
+				Some(compiled) => {
+					if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
+						parse_and_compile()?
+					} else {
+						(compiled, None)
+					}
+				}
+				None => parse_and_compile()?,
+			}
+		};
+
+		compiled.push((RefCell::new(cdoc), post));
+	}
+
+	for (doc, postprocess) in &compiled {
+		if postprocess.is_none() {
+			continue;
+		}
+
+		// Post processing
+		let body = postprocess
+			.as_ref()
+			.unwrap()
+			.apply(target, &compiled, &doc)?;
+		doc.borrow_mut().body = body;
+
+		// Insert into cache
+		doc.borrow().insert_cache(&con).map_err(|err| {
+			format!(
+				"Failed to insert compiled document from `{}` into cache: {err}",
+				doc.borrow().input
+			)
+		})?;
+	}
+
+	std::env::set_current_dir(current_dir)
+		.map_err(|err| format!("Failed to set current directory: {err}"))?;
+
+	Ok(compiled)
+}
+
+/// Processes sources from in-memory strings
+/// This function is indented for testing
+fn process_in_memory(target: Target, sources: Vec<String>) -> Result<Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>, String> {
+	let mut compiled = vec![];
+
+	let parser = LangParser::default();
+	for (idx, content) in sources.iter().enumerate() {
+		let parse_and_compile = || -> Result<(CompiledDocument, Option<PostProcess>), String> {
+			// Parse
+			let source = SourceFile::with_content(format!("{idx}"), content.clone(), None);
+			let doc = parse(&parser, Rc::new(source), &vec![])?;
+
+			// Compile
+			let compiler = Compiler::new(target, None);
+			let (mut compiled, postprocess) = compiler.compile(&*doc);
+
+			Ok((compiled, Some(postprocess)))
+		};
+
+		let (cdoc, post) = parse_and_compile()?;
+		compiled.push((RefCell::new(cdoc), post));
+	}
+
+	for (doc, postprocess) in &compiled {
+		if postprocess.is_none() {
+			continue;
+		}
+
+		// Post processing
+		let body = postprocess
+			.as_ref()
+			.unwrap()
+			.apply(target, &compiled, &doc)?;
+		doc.borrow_mut().body = body;
+	}
+
+	Ok(compiled)
+}
diff --git a/src/document/document.rs b/src/document/document.rs
index 4f94268..26c7222 100644
--- a/src/document/document.rs
+++ b/src/document/document.rs
@@ -22,7 +22,7 @@ pub enum ElemReference {
 	Nested(usize, usize),
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub enum CrossReference {
 	/// When the referenced document is unspecified
 	Unspecific(String),
diff --git a/src/document/references.rs b/src/document/references.rs
index 0a7e75e..fb379f5 100644
--- a/src/document/references.rs
+++ b/src/document/references.rs
@@ -45,7 +45,7 @@ pub mod tests {
 	use crate::parser::langparser::LangParser;
 	use crate::parser::parser::Parser;
 	use crate::parser::source::SourceFile;
-	use crate::ParserState;
+	use crate::parser::parser::ParserState;
 
 	#[test]
 	fn validate_refname_tests() {
diff --git a/src/elements/reference.rs b/src/elements/reference.rs
index f93bcb1..1d03d0e 100644
--- a/src/elements/reference.rs
+++ b/src/elements/reference.rs
@@ -91,13 +91,15 @@ impl Element for ExternalReference {
 		match compiler.target() {
 			Target::HTML => {
 				let mut result = "<a href=\"".to_string();
-				let refname = self.caption.as_ref().unwrap_or(match &self.reference {
-					CrossReference::Unspecific(name) => &name,
-					CrossReference::Specific(_, name) => &name,
-				});
 
 				compiler.insert_crossreference(cursor + result.len(), self.reference.clone());
-				result += format!("\">{}</a>", Compiler::sanitize(Target::HTML, refname)).as_str();
+
+				if let Some(caption) = &self.caption {
+					result +=
+						format!("\">{}</a>", Compiler::sanitize(Target::HTML, caption)).as_str();
+				} else {
+					result += format!("\">{}</a>", self.reference).as_str();
+				}
 				Ok(result)
 			}
 			_ => todo!(""),
@@ -268,10 +270,6 @@ impl RegexRule for ReferenceRule {
 			.and_then(|(_, s)| Some(s));
 
 		if let Some(refdoc) = refdoc {
-			if caption.is_none() {
-				return reports;
-			}
-
 			if refdoc.is_empty() {
 				state.push(
 					document,
@@ -305,3 +303,64 @@ impl RegexRule for ReferenceRule {
 		reports
 	}
 }
+
+#[cfg(test)]
+mod tests {
+	use crate::elements::paragraph::Paragraph;
+use crate::elements::section::Section;
+use crate::parser::langparser::LangParser;
+	use crate::parser::parser::Parser;
+use crate::parser::source::SourceFile;
+	use crate::validate_document;
+
+	use super::*;
+
+	#[test]
+	pub fn parse_internal() {
+		let source = Rc::new(SourceFile::with_content(
+			"".to_string(),
+			r#"
+#{ref} Referenceable section
+
+§{ref}[caption=Section]
+§{ref}[caption=Another]
+"#
+			.to_string(),
+			None,
+		));
+		let parser = LangParser::default();
+		let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
+
+		validate_document!(doc.content().borrow(), 0,
+			Section;
+			Paragraph {
+				InternalReference { refname == "ref", caption == Some("Section".to_string()) };
+				InternalReference { refname == "ref", caption == Some("Another".to_string()) };
+			};
+		);
+	}
+
+	#[test]
+	pub fn parse_external() {
+		let source = Rc::new(SourceFile::with_content(
+			"".to_string(),
+			r#"
+§{DocA#ref}[caption=Section]
+§{DocB#ref}
+§{#ref}[caption='ref' from any document]
+"#
+			.to_string(),
+			None,
+		));
+		let parser = LangParser::default();
+		let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
+
+		validate_document!(doc.content().borrow(), 0,
+			Paragraph {
+				ExternalReference { reference == CrossReference::Specific("DocA".into(), "ref".into()), caption == Some("Section".to_string()) };
+				ExternalReference { reference == CrossReference::Specific("DocB".into(), "ref".into()), caption == None::<String> };
+				ExternalReference { reference == CrossReference::Unspecific("ref".into()), caption == Some("'ref' from any document".to_string()) };
+			};
+		);
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index 798e863..c2d62a5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,29 +5,16 @@ mod elements;
 mod lua;
 mod parser;
 
-use std::cell::RefCell;
 use std::env;
 use std::io::BufWriter;
 use std::io::Write;
-use std::path::PathBuf;
 use std::process::ExitCode;
-use std::rc::Rc;
-use std::time::UNIX_EPOCH;
 
-use compiler::compiler::CompiledDocument;
-use compiler::compiler::Compiler;
 use compiler::compiler::Target;
 use compiler::navigation::create_navigation;
-use compiler::postprocess::PostProcess;
-use document::document::Document;
 use getopts::Options;
-use parser::langparser::LangParser;
-use parser::parser::Parser;
-use parser::parser::ParserState;
-use rusqlite::Connection;
 use walkdir::WalkDir;
 
-use crate::parser::source::SourceFile;
 extern crate getopts;
 
 fn print_usage(program: &str, opts: Options) {
@@ -49,142 +36,6 @@ NML version: 0.4\n"
 	);
 }
 
-fn parse(
-	parser: &LangParser,
-	input: &str,
-	debug_opts: &Vec<String>,
-) -> Result<Box<dyn Document<'static>>, String> {
-	println!("Parsing {input}...");
-
-	// Parse
-	let source = SourceFile::new(input.to_string(), None).unwrap();
-	let (doc, _) = parser.parse(ParserState::new(parser, None), Rc::new(source), None);
-
-	if debug_opts.contains(&"ast".to_string()) {
-		println!("-- BEGIN AST DEBUGGING --");
-		doc.content()
-			.borrow()
-			.iter()
-			.for_each(|elem| println!("{elem:#?}"));
-		println!("-- END AST DEBUGGING --");
-	}
-	if debug_opts.contains(&"ref".to_string()) {
-		println!("-- BEGIN REFERENCES DEBUGGING --");
-		let sc = doc.scope().borrow();
-		sc.referenceable.iter().for_each(|(name, reference)| {
-			println!(" - {name}: `{:#?}`", doc.get_from_reference(reference));
-		});
-		println!("-- END REFERENCES DEBUGGING --");
-	}
-	if debug_opts.contains(&"var".to_string()) {
-		println!("-- BEGIN VARIABLES DEBUGGING --");
-		let sc = doc.scope().borrow();
-		sc.variables.iter().for_each(|(_name, var)| {
-			println!(" - `{:#?}`", var);
-		});
-		println!("-- END VARIABLES DEBUGGING --");
-	}
-
-	if parser.has_error() {
-		return Err("Parsing failed due to errors while parsing".to_string());
-	}
-
-	Ok(doc)
-}
-
-fn process(
-	target: Target,
-	files: Vec<PathBuf>,
-	db_path: &Option<String>,
-	force_rebuild: bool,
-	debug_opts: &Vec<String>,
-) -> Result<Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>, String> {
-	let mut compiled = vec![];
-
-	let current_dir = std::env::current_dir()
-		.map_err(|err| format!("Unable to get the current working directory: {err}"))?;
-
-	let con = db_path
-		.as_ref()
-		.map_or(Connection::open_in_memory(), |path| Connection::open(path))
-		.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
-	CompiledDocument::init_cache(&con)
-		.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
-
-	let parser = LangParser::default();
-	for file in files {
-		let meta = std::fs::metadata(&file)
-			.map_err(|err| format!("Failed to get metadata for `{file:#?}`: {err}"))?;
-
-		let modified = meta
-			.modified()
-			.map_err(|err| format!("Unable to query modification time for `{file:#?}`: {err}"))?;
-
-		// Move to file's directory
-		let file_parent_path = file
-			.parent()
-			.ok_or(format!("Failed to get parent path for `{file:#?}`"))?;
-		std::env::set_current_dir(file_parent_path)
-			.map_err(|err| format!("Failed to move to path `{file_parent_path:#?}`: {err}"))?;
-
-		let parse_and_compile = || -> Result<(CompiledDocument, Option<PostProcess>), String> {
-			// Parse
-			let doc = parse(&parser, file.to_str().unwrap(), debug_opts)?;
-
-			// Compile
-			let compiler = Compiler::new(target, db_path.clone());
-			let (mut compiled, postprocess) = compiler.compile(&*doc);
-
-			compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
-
-			Ok((compiled, Some(postprocess)))
-		};
-
-		let (cdoc, post) = if force_rebuild {
-			parse_and_compile()?
-		} else {
-			match CompiledDocument::from_cache(&con, file.to_str().unwrap()) {
-				Some(compiled) => {
-					if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
-						parse_and_compile()?
-					} else {
-						(compiled, None)
-					}
-				}
-				None => parse_and_compile()?,
-			}
-		};
-
-		compiled.push((RefCell::new(cdoc), post));
-	}
-
-	for (doc, postprocess) in &compiled {
-		if postprocess.is_none() {
-			continue;
-		}
-
-		// Post processing
-		let body = postprocess
-			.as_ref()
-			.unwrap()
-			.apply(target, &compiled, &doc)?;
-		doc.borrow_mut().body = body;
-
-		// Insert into cache
-		doc.borrow().insert_cache(&con).map_err(|err| {
-			format!(
-				"Failed to insert compiled document from `{}` into cache: {err}",
-				doc.borrow().input
-			)
-		})?;
-	}
-
-	std::env::set_current_dir(current_dir)
-		.map_err(|err| format!("Failed to set current directory: {err}"))?;
-
-	Ok(compiled)
-}
-
 fn main() -> ExitCode {
 	let args: Vec<String> = env::args().collect();
 	let program = args[0].clone();
@@ -362,13 +213,15 @@ fn main() -> ExitCode {
 	}
 
 	// Parse, compile using the cache
-	let processed = match process(Target::HTML, files, &db_path, force_rebuild, &debug_opts) {
-		Ok(processed) => processed,
-		Err(e) => {
-			eprintln!("{e}");
-			return ExitCode::FAILURE;
-		}
-	};
+	let processed =
+		match compiler::process::process(Target::HTML, files, &db_path, force_rebuild, &debug_opts)
+		{
+			Ok(processed) => processed,
+			Err(e) => {
+				eprintln!("{e}");
+				return ExitCode::FAILURE;
+			}
+		};
 
 	if input_meta.is_dir()
 	// Batch mode
diff --git a/style.css b/style.css
index a79d686..70e08dc 100644
--- a/style.css
+++ b/style.css
@@ -137,6 +137,11 @@ a.inline-code {
 	content: "–";
 }
 
+/* Sections */
+a.section-link {
+	text-decoration: none;
+}
+
 /* Code blocks */
 div.code-block-title {
 	background-color: #20202a;
@@ -241,3 +246,28 @@ a:hover.medium-ref img {
 
 	box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.75);
 }
+
+/* Blockquote */
+blockquote {
+	margin-left: 0.2em;
+	padding-left: 0.6em;
+
+	border-left: 4px solid #0ff08b;
+}
+
+blockquote p::before {
+	content: '\201C';
+}
+
+blockquote p::after {
+	content: '\201D';
+}
+
+.blockquote-author:before {
+	content: '—';
+}
+
+.blockquote-author {
+	margin-left: 0.2em;
+}
+