Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/orgmode/reference/config.org
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,15 @@ Adding =algorithm= stops reflow of that environment.

String array of extra command names tokenized like the built-in verb command before sentence split.
The next character after the name is the delimiter.
Built-in names are =verb=, =lstinline=, =lstinputlisting=, =spverb=, =mintinline=, =inputminted=, =mint=, fancyvrb =Verb= / =Verb*= / =SaveVerb= / =VerbatimInput= / =BVerbatimInput= / =LVerbatimInput=, piton.sty =\piton=, and tools/verbatim.sty =\verbatiminput=; =lstinline= still accepts optional =[...]= and ={...}=.
Built-in names are =verb=, =lstinline=, =lstinputlisting=, =spverb=, =mintinline=, =inputminted=, =mint=, fancyvrb =Verb= / =Verb*= / =SaveVerb= / =VerbatimInput= / =BVerbatimInput= / =LVerbatimInput=, piton.sty =\piton= / =\PitonInputFile=, and tools/verbatim.sty =\verbatiminput=; =lstinline= still accepts optional =[...]= and ={...}=.
=mintinline= / =mint= take optional =[...]=, a ={lang}= argument, then a delimiter or ={...}= body.
=\inputminted= takes the same optional =[...]= and ={lang}= then a required ={filename}=; following flush prose stays on its own line.
=SaveVerb= takes optional =[...]=, a ={name}= argument, then the same delimiter body as =Verb=.
=\piton|...|= is verb-like (interior =.!?%= stay one token); =\piton{...}= stays one token via the generic command argument.
=\lstinputlisting= takes optional =[...]= then a required ={filename}=; following flush prose stays on its own line.
=\VerbatimInput= / =\BVerbatimInput= / =\LVerbatimInput= take optional =[...]= then a required ={filename}=; following flush prose stays on its own line.
=\verbatiminput= / =\verbatiminput*= take a required ={filename}=; following flush prose stays on its own line.
=\PitonInputFile= takes optional =<...>= / =[...]= then a required ={filename}=; following flush prose stays on its own line.
Adding another name keeps that command's delimited body atomic and treats an inner =%= as content, not a comment.

* Code-block sections (=[code.<lang>]=)
Expand Down
3 changes: 2 additions & 1 deletion docs/orgmode/reference/formats.org
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,13 @@ These tokens within prose are not split across lines:
- piton.sty =Piton= is a built-in code region (verbatim listing env)
- Extra names from =[latex].verbatim_envs= are code regions too
- Body follows the same comment-reflow and optional =--format-code= rules as other formats when language is known (=minted= / =minted*= language arg, =lstlisting= / =lstlisting*= =language== option)
- Inline =\verb= / =\lstinline= / =\spverb= / =\mintinline= / =\mint= / fancyvrb =\Verb= / =\Verb*= / =\SaveVerb= / =\VerbatimInput= / =\BVerbatimInput= / =\LVerbatimInput= / piton.sty =\piton= / listings.sty =\lstinputlisting= / minted.sty =\inputminted= / tools/verbatim.sty =\verbatiminput= (and extra =[latex].verbatim_commands=) stay atomic; inner =.!?%= do not split or comment.
- Inline =\verb= / =\lstinline= / =\spverb= / =\mintinline= / =\mint= / fancyvrb =\Verb= / =\Verb*= / =\SaveVerb= / =\VerbatimInput= / =\BVerbatimInput= / =\LVerbatimInput= / piton.sty =\piton= / =\PitonInputFile= / listings.sty =\lstinputlisting= / minted.sty =\inputminted= / tools/verbatim.sty =\verbatiminput= (and extra =[latex].verbatim_commands=) stay atomic; inner =.!?%= do not split or comment.
=\piton|...|= is verb-like; =\piton{...}= stays one token via the generic command argument
=\lstinputlisting= / =\lstinputlisting*= take optional =[...]= then a ={filename}=; a flush following sentence stays on its own line
=\inputminted= / =\inputminted*= take optional =[...]=, ={lang}=, then a ={filename}=; a flush following sentence stays on its own line
=\VerbatimInput= / =\BVerbatimInput= / =\LVerbatimInput= take optional =[...]= then a ={filename}=; a flush following sentence stays on its own line
=\verbatiminput= / =\verbatiminput*= take a ={filename}=; a flush following sentence stays on its own line
=\PitonInputFile= takes optional =<...>=, optional =[...]=, then a ={filename}=; a flush following sentence stays on its own line

*** Prose regions (reflowed)
:PROPERTIES:
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub struct FormatOverrides {
pub structure_envs: Vec<String>,
/// Extra LaTeX command names tokenized like `\verb` before split.
/// Meaningful under `[latex]` only. Missing or empty keeps
/// verb/lstinline/lstinputlisting/spverb/mintinline/inputminted/mint/Verb/SaveVerb/VerbatimInput/piton/verbatiminput.
/// verb/lstinline/lstinputlisting/spverb/mintinline/inputminted/mint/Verb/SaveVerb/VerbatimInput/piton/verbatiminput/PitonInputFile.
pub verbatim_commands: Vec<String>,
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ pub struct FormatConfig {
/// to `NON_PROSE_ENVS`. Empty keeps the built-in list.
pub latex_structure_envs: Vec<String>,
/// Extra LaTeX command names tokenized like `\verb` before split.
/// Empty keeps verb/lstinline/lstinputlisting/spverb/mintinline/inputminted/mint/Verb/SaveVerb/VerbatimInput/piton/verbatiminput.
/// Empty keeps verb/lstinline/lstinputlisting/spverb/mintinline/inputminted/mint/Verb/SaveVerb/VerbatimInput/piton/verbatiminput/PitonInputFile.
pub latex_verbatim_commands: Vec<String>,
}

Expand Down
208 changes: 206 additions & 2 deletions src/parser/latex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ impl LatexParser {
/// inside `\verb` / `\lstinline` / `\spverb` / `\mintinline` / `\mint` /
/// `\Verb` / `\SaveVerb` / `\piton` / `\lstinputlisting` /
/// `\inputminted` / `\verbatiminput` / `\VerbatimInput` /
/// configured verbatim commands.
/// `\PitonInputFile` / configured verbatim commands.
fn unescaped_percent(&self, line: &str) -> Option<usize> {
unescaped_percent_with(line, &self.extra_verbatim_commands)
}
Expand Down Expand Up @@ -877,7 +877,7 @@ fn find_tex_cs(line: &str, from: usize, cs: &str) -> Option<usize> {
/// `\iffalse` in ordinary TeX, skipping `\verb` / `\lstinline` /
/// `\spverb` / `\mintinline` / `\mint` / `\inputminted` / `\Verb` /
/// `\SaveVerb` / `\piton` / `\lstinputlisting` / `\verbatiminput` /
/// `\VerbatimInput` spans.
/// `\VerbatimInput` / `\PitonInputFile` spans.
fn find_iffalse_at(line: &str, from: usize, extra_cmds: &[String]) -> Option<usize> {
let bytes = line.as_bytes();
let mut i = from;
Expand Down Expand Up @@ -941,6 +941,19 @@ fn find_fancyvrb_input_at(
find_leftover_cmd_at(line, from, extra_cmds, fancyvrb_input_cs_at)
}

/// Leftover piton.sty `\PitonInputFile` (optional `<...>`, optional
/// `[...]`, required `{file}`; `d < > O { } m`; GitHub #406). Other
/// verb spans are skipped so `\verb|\PitonInputFile{x}|` is not
/// stolen. Walk stops at an unescaped `%` so a comment is not a
/// command tail.
fn find_pitoninputfile_at(
line: &str,
from: usize,
extra_cmds: &[String],
) -> Option<(usize, usize)> {
find_leftover_cmd_at(line, from, extra_cmds, pitoninputfile_cs_at)
}

fn find_leftover_cmd_at(
line: &str,
from: usize,
Expand Down Expand Up @@ -1013,6 +1026,16 @@ fn fancyvrb_input_cs_at(line: &str, at: usize) -> bool {
false
}

fn pitoninputfile_cs_at(line: &str, at: usize) -> bool {
let Some(tail) = line.get(at..).and_then(|s| s.strip_prefix('\\')) else {
return false;
};
let Some(after) = tail.strip_prefix("PitonInputFile") else {
return false;
};
!after.starts_with(|c: char| c.is_ascii_alphabetic())
}

struct ParseState<'a> {
input: &'a str,
parser: &'a LatexParser,
Expand Down Expand Up @@ -1704,6 +1727,9 @@ impl<'a> ParseState<'a> {
.or_else(|| {
find_fancyvrb_input_at(code, i, &self.parser.extra_verbatim_commands)
})
.or_else(|| {
find_pitoninputfile_at(code, i, &self.parser.extra_verbatim_commands)
})
{
self.append_item_or_prose(line.start + i, &code[i..start]);
self.push_structure(ByteSpan::new(
Expand Down Expand Up @@ -2875,6 +2901,184 @@ Some text.
assert_eq!(format_text(&minted_out, &latex_cfg()).unwrap(), minted_out);
}

/// Ticket fixture (GitHub #406): piton.sty `\PitonInputFile{file}`
/// is one leftover command. Following flush prose does not join the
/// command line. `After.` / `Next.` still split. Optional `[...]`
/// and `d < >` stay atomic. Piton env, `\piton`, `\verbatiminput`,
/// and fancyvrb `\VerbatimInput` unchanged.
#[test]
fn pitoninputfile_does_not_join_following_prose() {
use crate::format_text;

let input = concat!(
"Before. Next.\n",
"\\PitonInputFile{foo.py}\n",
"After. Next.\n",
);
let regions = LatexParser::default().parse(input);
assert!(
regions.iter().any(|r| matches!(
r,
Region::Structure(s) if s.contains(r"\PitonInputFile{foo.py}")
)),
"PitonInputFile must stay one Structure command, got: {regions:?}"
);
assert!(
!regions.iter().any(|r| matches!(
r,
Region::Prose(p) if p.contains(r"\PitonInputFile{foo.py}")
)),
"PitonInputFile must not leak into Prose, got: {regions:?}"
);
assert!(
regions.iter().any(|r| matches!(
r,
Region::Prose(p) if p.contains("After.") && p.contains("Next.")
)),
"After. / Next. must stay Prose, got: {regions:?}"
);
let out = format_text(input, &latex_cfg()).unwrap();
assert!(
out.contains("\\PitonInputFile{foo.py}\n"),
"PitonInputFile must stay one atomic command, got:\n{out}"
);
assert!(
!out.contains("\\PitonInputFile{foo.py} After."),
"following flush prose must not join the command line, got:\n{out}"
);
assert!(
out.contains("Before.\nNext."),
"prose before PitonInputFile must still split, got:\n{out}"
);
assert!(
out.contains("After.\nNext."),
"prose after PitonInputFile must still split, got:\n{out}"
);
assert_eq!(format_text(&out, &latex_cfg()).unwrap(), out);

let opts = concat!(
"Before. Next.\n",
"\\PitonInputFile[language=python]{foo.py}\n",
"After. Next.\n",
);
let opts_out = format_text(opts, &latex_cfg()).unwrap();
assert!(
opts_out.contains("\\PitonInputFile[language=python]{foo.py}\n"),
"PitonInputFile optional args must stay atomic, got:\n{opts_out}"
);
assert!(
!opts_out.contains("\\PitonInputFile[language=python]{foo.py} After."),
"optional-arg PitonInputFile must not join following prose, got:\n{opts_out}"
);
assert!(
opts_out.contains("After.\nNext."),
"prose after optional-arg PitonInputFile must still split, got:\n{opts_out}"
);

let range = concat!(
"Before. Next.\n",
"\\PitonInputFile<1-10>[language=python]{foo.py}\n",
"After. Next.\n",
);
let range_out = format_text(range, &latex_cfg()).unwrap();
assert!(
range_out.contains("\\PitonInputFile<1-10>[language=python]{foo.py}\n"),
"PitonInputFile d<> plus optional args must stay atomic, got:\n{range_out}"
);
assert!(
!range_out.contains("\\PitonInputFile<1-10>[language=python]{foo.py} After."),
"d<> PitonInputFile must not join following prose, got:\n{range_out}"
);
assert!(
range_out.contains("After.\nNext."),
"prose after d<> PitonInputFile must still split, got:\n{range_out}"
);

let angle = concat!(
"Before. Next.\n",
"\\PitonInputFile<python>{foo.py}\n",
"After. Next.\n",
);
let angle_out = format_text(angle, &latex_cfg()).unwrap();
assert!(
angle_out.contains("\\PitonInputFile<python>{foo.py}\n"),
"PitonInputFile standalone angle must stay atomic, got:\n{angle_out}"
);
assert!(
!angle_out.contains("\\PitonInputFile<python>{foo.py} After."),
"standalone-angle PitonInputFile must not join following prose, got:\n{angle_out}"
);
assert!(
angle_out.contains("After.\nNext."),
"prose after standalone-angle PitonInputFile must still split, got:\n{angle_out}"
);

let piton_env = concat!(
"\\begin{Piton}\n",
"First line. Second line.\n",
"\\end{Piton}\n",
"After the block. Next.\n",
);
let piton_env_out = format_text(piton_env, &latex_cfg()).unwrap();
assert!(
piton_env_out.contains("\\begin{Piton}\nFirst line. Second line.\n\\end{Piton}"),
"Piton env must stay a code env, got:\n{piton_env_out}"
);
assert!(
piton_env_out.contains("After the block.\nNext."),
"prose after Piton env must still split, got:\n{piton_env_out}"
);

let piton_cmd = "See \\piton|done. Next| here. After.\n";
let piton_cmd_out = format_text(piton_cmd, &latex_cfg()).unwrap();
assert!(
piton_cmd_out.contains("See \\piton|done. Next| here.\nAfter."),
"\\piton must stay intact and still split, got:\n{piton_cmd_out}"
);

let verb_in = concat!(
"Before. Next.\n",
"\\verbatiminput{foo.py}\n",
"After. Next.\n",
);
let verb_out = format_text(verb_in, &latex_cfg()).unwrap();
assert!(
verb_out.contains("\\verbatiminput{foo.py}\n"),
"verbatiminput must stay unchanged, got:\n{verb_out}"
);
assert!(
!verb_out.contains("\\verbatiminput{foo.py} After."),
"verbatiminput must not join following prose, got:\n{verb_out}"
);
assert!(
verb_out.contains("After.\nNext."),
"prose after verbatiminput must still split, got:\n{verb_out}"
);

let fancy = concat!(
"Before. Next.\n",
"\\VerbatimInput{foo.py}\n",
"After. Next.\n",
);
let fancy_out = format_text(fancy, &latex_cfg()).unwrap();
assert!(
fancy_out.contains("\\VerbatimInput{foo.py}\n"),
"VerbatimInput must stay unchanged, got:\n{fancy_out}"
);
assert!(
!fancy_out.contains("\\VerbatimInput{foo.py} After."),
"VerbatimInput must not join following prose, got:\n{fancy_out}"
);
assert!(
!fancy_out.contains("\\PitonInputFile"),
"PitonInputFile walker must not rewrite VerbatimInput, got:\n{fancy_out}"
);
assert!(
fancy_out.contains("After.\nNext."),
"prose after VerbatimInput must still split, got:\n{fancy_out}"
);
}

/// Ticket fixture (GitHub #245): minted `\mintinline{lang}|body|` is
/// one token; following `Next sentence.` still splits.
#[test]
Expand Down
Loading
Loading