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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 10:59:27 -04:00
1 parent 88b0f8f5fc
commit bfd507ae2e
10 files changed
+87 -16

No files matched your search

+5 -5
View File
@@ -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}");
+8 -2
View File
@@ -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()),
}
+6 -2
View File
@@ -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(),
+6 -2
View File
@@ -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()
+50
View File
@@ -403,3 +403,53 @@ mod files {
}
}
}
#[test]
fn a_disguised_script_link_does_not_follow() {
let text = "[a](< javascript:alert(1)>) [b](java&#x09;script:alert(1)) [c](java&#10;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,
"![d](dot.png)\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
);
}