C14.14 r2: links judged as browsers read them; only a folder this export made is removed; formula text outlined or refused (library 3bbbd05)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
88b0f8f5fc
commit
bfd507ae2e
10 files changed
+87
-16
No files matched your search
@@ -6,7 +6,7 @@ use std::{
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use super::{Page, Pictures, body, document, plain};
|
||||
use super::{Page, Pictures, body, document, page_of, pictures::Placed, plain};
|
||||
|
||||
/// What an export writes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -64,13 +64,13 @@ fn export(target: &Target, bytes: &[u8]) -> io::Result<()> {
|
||||
/// The page beside a new folder of its pictures.
|
||||
fn linked(text: &str, beside: Option<&Path>, target: &Target, title: &str) -> io::Result<Page> {
|
||||
let folder = target.path.with_file_name(format!("{title}_files"));
|
||||
let found = folder.exists();
|
||||
let written = body(text, beside, &Pictures::Folder(folder.clone()))
|
||||
let pictures = Pictures::Folder(folder.clone());
|
||||
let mut placed = Placed::new(beside, &pictures);
|
||||
let written = page_of(text, &mut placed)
|
||||
.and_then(|page| export(target, document(title, &page.html).as_bytes()).map(|()| page));
|
||||
// A failure takes only the folder it made.
|
||||
if written.is_err()
|
||||
&& !found
|
||||
&& folder.exists()
|
||||
&& placed.made
|
||||
&& let Err(left) = std::fs::remove_dir_all(&folder)
|
||||
{
|
||||
log::error!("mark: the half-made picture folder stays: {left}");
|
||||
|
||||
@@ -258,9 +258,15 @@ fn bare(svg: &str) -> &str {
|
||||
svg.find("<svg").map_or(svg, |at| &svg[at..])
|
||||
}
|
||||
|
||||
/// A plain, web or mail address.
|
||||
/// A plain, web or mail address, as a browser reads it.
|
||||
fn safe(url: &str) -> bool {
|
||||
match scheme(url) {
|
||||
// Browsers trim spaces and controls, then drop tabs and newlines.
|
||||
let read: String = url
|
||||
.trim_matches(|ch: char| ch <= ' ')
|
||||
.chars()
|
||||
.filter(|ch| !matches!(ch, '\t' | '\n' | '\r'))
|
||||
.collect();
|
||||
match scheme(&read) {
|
||||
None => true,
|
||||
Some(scheme) => ["http", "https", "mailto"].contains(&scheme.to_lowercase().as_str()),
|
||||
}
|
||||
|
||||
@@ -37,11 +37,15 @@ img,svg{max-width:100%}div.math{text-align:center}span.math svg{vertical-align:m
|
||||
|
||||
/// `text`'s body as HTML; pictures go as `pictures` says.
|
||||
pub fn body(text: &str, beside: Option<&Path>, pictures: &Pictures) -> io::Result<Page> {
|
||||
page_of(text, &mut pictures::Placed::new(beside, pictures))
|
||||
}
|
||||
|
||||
/// `text`'s body; `placed` keeps what it copied.
|
||||
fn page_of(text: &str, placed: &mut pictures::Placed) -> io::Result<Page> {
|
||||
let tree = markdown::to_mdast(text, &markdown_options())
|
||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||
let mut page = Page::default();
|
||||
let mut placed = pictures::Placed::new(beside, pictures);
|
||||
html::write(&tree, &mut placed, &mut page)?;
|
||||
html::write(&tree, placed, &mut page)?;
|
||||
log::info!(
|
||||
"mark: a page of {} bytes, {} pictures copied, {} missing, {} diagrams and {} formulas as code",
|
||||
page.html.len(),
|
||||
|
||||
@@ -36,6 +36,8 @@ pub(super) struct Placed<'a> {
|
||||
pictures: &'a Pictures,
|
||||
/// Pictures copied so far, and their names.
|
||||
pub copied: Vec<(PathBuf, String)>,
|
||||
/// This export made the picture folder.
|
||||
pub made: bool,
|
||||
}
|
||||
|
||||
impl<'a> Placed<'a> {
|
||||
@@ -44,6 +46,7 @@ impl<'a> Placed<'a> {
|
||||
beside,
|
||||
pictures,
|
||||
copied: Vec::new(),
|
||||
made: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +120,8 @@ impl<'a> Placed<'a> {
|
||||
}
|
||||
|
||||
/// Copies `source` under a name still free.
|
||||
fn copy(&self, source: &mut File, path: &Path, folder: &Path) -> io::Result<String> {
|
||||
if self.copied.is_empty() {
|
||||
fn copy(&mut self, source: &mut File, path: &Path, folder: &Path) -> io::Result<String> {
|
||||
if !self.made {
|
||||
// A folder already there is someone's: refused.
|
||||
std::fs::create_dir(folder).map_err(|error| match error.kind() {
|
||||
io::ErrorKind::AlreadyExists => {
|
||||
@@ -126,6 +129,7 @@ impl<'a> Placed<'a> {
|
||||
}
|
||||
_ => error,
|
||||
})?;
|
||||
self.made = true;
|
||||
}
|
||||
let file = path
|
||||
.file_name()
|
||||
|
||||
@@ -403,3 +403,53 @@ mod files {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disguised_script_link_does_not_follow() {
|
||||
let text = "[a](< javascript:alert(1)>) [b](java	script:alert(1)) [c](java script:alert(1)) [d](< https://x.org>)\n";
|
||||
let html = page(text, None, &Pictures::Inline).html;
|
||||
assert_eq!(html.matches("<a>").count(), 3, "{html}");
|
||||
assert!(html.contains("href=\" https://x.org\""), "{html}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_linked_page_takes_the_folder_it_made() {
|
||||
let dir = tempfile::tempdir().expect("a dir");
|
||||
std::fs::write(dir.path().join("dot.png"), b"png").expect("writes");
|
||||
// A folder where the page goes: the page cannot be written.
|
||||
let out = dir.path().join("out.html");
|
||||
std::fs::create_dir(&out).expect("makes");
|
||||
let target = super::Target {
|
||||
path: &out,
|
||||
session: None,
|
||||
revision: 1,
|
||||
work: dir.path(),
|
||||
};
|
||||
super::write(
|
||||
super::Format::Linked,
|
||||
"\n",
|
||||
Some(dir.path()),
|
||||
&target,
|
||||
)
|
||||
.expect_err("fails");
|
||||
assert!(
|
||||
!dir.path().join("out_files").exists(),
|
||||
"the folder it made is gone"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_formula_letter_no_font_has_stays_as_tex() {
|
||||
let page = page(
|
||||
"Before\n\n$\text{\u{F0000}}$ after.\n",
|
||||
None,
|
||||
&Pictures::Inline,
|
||||
);
|
||||
let lines: Vec<usize> = page.formulas.iter().map(|(line, _)| *line).collect();
|
||||
assert_eq!(lines, [3]);
|
||||
assert!(
|
||||
page.html.contains("<code>\text{\u{F0000}}</code>"),
|
||||
"{}",
|
||||
page.html
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user