1use std::cell::RefCell;
4use std::rc::Rc;
5use texlang::parse::{FileLocation, OptionalEquals};
6use texlang::prelude as txl;
7use texlang::token::lexer;
8use texlang::token::trace;
9use texlang::traits::*;
10use texlang::*;
11use texlang_common as common;
12
13use crate::conditional::{self, Condition};
14
15pub fn get_input<S: TexlangState + common::HasFileSystem>() -> command::BuiltIn<S> {
17 command::BuiltIn::new_expansion(input_fn)
18}
19
20fn input_fn<S: TexlangState + common::HasFileSystem>(
21 input_token: token::Token,
22 input: &mut vm::ExpansionInput<S>,
23) -> txl::Result<()> {
24 let file_location = FileLocation::parse(input)?;
25 let (file_path, source_code) =
26 match texlang_common::read_file_to_string(input.vm(), file_location, "tex") {
27 Ok(ok) => ok,
28 Err(err) => {
29 return Err(input.fatal_error(err));
30 }
31 };
32 if input.vm().num_current_sources() > 100 {
33 return Err(input.fatal_error(TooManyInputs {}));
34 }
35 input.push_source(input_token, file_path, source_code)?;
36 Ok(())
37}
38
39#[derive(Debug)]
40struct TooManyInputs {}
41
42impl error::TexError for TooManyInputs {
43 fn kind(&self) -> error::Kind {
44 error::Kind::FailedPrecondition
45 }
46
47 fn title(&self) -> String {
48 "too many input levels (100)".into()
49 }
50}
51
52pub fn get_endinput<S: TexlangState>() -> command::BuiltIn<S> {
54 command::BuiltIn::new_expansion(endinput_fn)
55}
56
57fn endinput_fn<S: TexlangState>(
58 _: token::Token,
59 input: &mut vm::ExpansionInput<S>,
60) -> txl::Result<()> {
61 input.end_current_file();
62 Ok(())
63}
64
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct Component<const N: usize> {
67 #[cfg_attr(feature = "serde", serde(with = "texcraft_stdext::serde_tools::array"))]
68 files: [Option<Box<lexer::Lexer>>; N],
69}
70
71impl<const N: usize> Component<N> {
72 fn take_file(&mut self, index: i32) -> Option<Box<lexer::Lexer>> {
73 let u: usize = match index.try_into() {
74 Ok(u) => u,
75 Err(_) => return None,
76 };
77 match self.files.get_mut(u) {
78 None => None,
79 Some(file_or) => file_or.take(),
80 }
81 }
82
83 fn return_file(&mut self, index: i32, file: Box<lexer::Lexer>) {
84 let u: usize = index.try_into().unwrap();
85 *self.files.get_mut(u).unwrap() = Some(file);
86 }
87}
88
89impl<const N: usize> Default for Component<N> {
90 fn default() -> Self {
91 let v: Vec<Option<Box<lexer::Lexer>>> = (0..N).map(|_| None).collect();
96 Self {
97 files: v.try_into().unwrap(),
98 }
99 }
100}
101
102pub fn get_openin<const N: usize, S: HasComponent<Component<N>> + common::HasFileSystem>(
104) -> command::BuiltIn<S> {
105 command::BuiltIn::new_execution(openin_fn)
106}
107
108fn openin_fn<const N: usize, S: HasComponent<Component<N>> + common::HasFileSystem>(
109 openin_token: token::Token,
110 input: &mut vm::ExecutionInput<S>,
111) -> txl::Result<()> {
112 let (u, _, file_location) = <(parse::Uint<N>, OptionalEquals, FileLocation)>::parse(input)?;
113 let lexer_or = match texlang_common::read_file_to_string(input.vm(), file_location, "tex") {
114 Err(_) => None,
117 Ok((file_path, source_code)) => {
118 let source_code = ensure_ends_in_newline(source_code);
119 let trace_key_range = input.tracer_mut().register_source_code(
120 Some(openin_token),
121 trace::Origin::File(file_path),
122 &source_code,
123 );
124 Some(Box::new(lexer::Lexer::new(source_code, trace_key_range)))
125 }
126 };
127 *input
128 .state_mut()
129 .component_mut()
130 .files
131 .get_mut(u.0)
132 .unwrap() = lexer_or;
133 Ok(())
134}
135
136fn ensure_ends_in_newline(mut s: String) -> String {
137 if !s.ends_with('\n') {
138 s.push('\n')
139 }
140 s
141}
142
143pub fn get_closein<const N: usize, S: HasComponent<Component<N>>>() -> command::BuiltIn<S> {
145 command::BuiltIn::new_execution(closein_fn)
146}
147
148fn closein_fn<const N: usize, S: HasComponent<Component<N>>>(
149 _: token::Token,
150 input: &mut vm::ExecutionInput<S>,
151) -> txl::Result<()> {
152 let u = parse::Uint::<N>::parse(input)?;
153 *input
154 .state_mut()
155 .component_mut()
156 .files
157 .get_mut(u.0)
158 .unwrap() = None;
159 Ok(())
160}
161
162pub fn get_read<const N: usize, S: HasComponent<Component<N>> + common::HasTerminalIn>(
164) -> command::BuiltIn<S> {
165 command::BuiltIn::new_execution(read_fn)
166}
167
168fn read_fn<const N: usize, S: HasComponent<Component<N>> + common::HasTerminalIn>(
169 _: token::Token,
170 input: &mut vm::ExecutionInput<S>,
171) -> txl::Result<()> {
172 let scope = TexlangState::variable_assignment_scope_hook(input.state_mut());
173 let (index, _, cmd_ref_or) = <(i32, To, Option<token::CommandRef>)>::parse(input)?;
174
175 #[derive(Copy, Clone, PartialEq, Eq)]
176 enum Mode {
177 File,
178 Terminal,
179 }
180
181 let terminal_in = input.state().terminal_in().clone();
182 let (mut lexer, mode, prompt) = match input.state_mut().component_mut().take_file(index) {
183 Some(file) => (file, Mode::File, None),
184 None => {
185 let prompt = if index < 0 {
186 None
187 } else {
188 Some(format!(
189 r"{}=",
190 match cmd_ref_or {
191 None => "unreachable".to_string(),
192 Some(cmd_ref) => cmd_ref.to_string(input.vm().cs_name_interner()),
193 }
194 ))
195 };
196 (
197 read_from_terminal(&terminal_in, input, &prompt)?,
198 Mode::Terminal,
199 prompt,
200 )
201 }
202 };
203
204 let mut tokens = vec![];
205 let mut more_lines_exist = true;
206 let mut braces: Vec<token::Token> = vec![];
207 loop {
208 let vm::Parts {
209 state,
210 cs_name_interner,
211 ..
212 } = input.vm_parts();
213 match (lexer.next(state, cs_name_interner, true), mode) {
214 (lexer::Result::Token(token), _) => {
215 match token.cat_code() {
216 Some(types::CatCode::BeginGroup) => {
217 braces.push(token);
218 }
219 Some(types::CatCode::EndGroup) => {
220 if braces.pop().is_none() {
221 more_lines_exist = drain_line(&mut lexer, state, cs_name_interner);
222 break;
223 }
224 }
225 _ => (),
226 };
227 tokens.push(token);
228 }
229 (lexer::Result::InvalidCharacter(c, trace_key), _) => {
230 return Err(input.fatal_error(lexer::InvalidCharacterError::new(
231 input.vm(),
232 c,
233 trace_key,
234 )))
235 }
236 (lexer::Result::EndOfLine, Mode::File) => {
237 if braces.is_empty() {
238 break;
239 }
240 }
241 (lexer::Result::EndOfInput, Mode::File) => {
242 if let Some(unmatched_brace) = braces.pop() {
243 return Err(input.fatal_error(UnmatchedBracesError { unmatched_brace }));
244 }
245 more_lines_exist = false;
246 break;
247 }
248 (lexer::Result::EndOfLine | lexer::Result::EndOfInput, Mode::Terminal) => {
249 if !braces.is_empty() {
250 lexer = read_from_terminal(&terminal_in, input, &prompt)?;
251 continue;
252 }
253 break;
254 }
255 }
256 }
257 if mode == Mode::File && more_lines_exist {
258 input.state_mut().component_mut().return_file(index, lexer);
259 }
260 tokens.reverse();
261 let user_defined_macro =
262 texmacro::Macro::new(vec![], vec![], vec![texmacro::Replacement::Tokens(tokens)]);
263 if let Some(cmd_ref) = cmd_ref_or {
264 input
265 .commands_map_mut()
266 .insert_macro(cmd_ref, user_defined_macro, scope);
267 }
268 Ok(())
269}
270
271fn drain_line<S: TexlangState>(
272 file: &mut lexer::Lexer,
273 state: &S,
274 cs_name_interner: &mut token::CsNameInterner,
275) -> bool {
276 loop {
277 match file.next(state, cs_name_interner, true) {
278 lexer::Result::Token(_) | lexer::Result::InvalidCharacter(_, _) => {}
279 lexer::Result::EndOfLine => {
280 return true;
281 }
282 lexer::Result::EndOfInput => {
283 return false;
284 }
285 }
286 }
287}
288
289fn read_from_terminal<S: TexlangState>(
290 terminal_in: &Rc<RefCell<dyn common::TerminalIn>>,
291 input: &mut vm::ExecutionInput<S>,
292 prompt: &Option<String>,
293) -> txl::Result<Box<lexer::Lexer>> {
294 let mut buffer = String::new();
295 if let Err(err) = terminal_in
296 .borrow_mut()
297 .read_line(prompt.as_deref(), &mut buffer)
298 {
299 return Err(input.fatal_error(IoError {
300 title: "failed to read from the terminal".into(),
301 underlying_error: err,
302 }));
303 }
304 let trace_key_range =
305 input
306 .tracer_mut()
307 .register_source_code(None, trace::Origin::Terminal, &buffer);
308 Ok(Box::new(lexer::Lexer::new(buffer, trace_key_range)))
309}
310
311pub fn get_ifeof<const N: usize, S>() -> command::BuiltIn<S>
313where
314 S: HasComponent<Component<N>> + HasComponent<conditional::Component>,
315{
316 IsEof::build_if_command()
317}
318
319struct IsEof<const N: usize>;
320
321impl<const N: usize, S> conditional::Condition<S> for IsEof<N>
322where
323 S: HasComponent<Component<N>> + HasComponent<conditional::Component>,
324{
325 fn evaluate(input: &mut vm::ExpansionInput<S>) -> txl::Result<bool> {
326 let u = parse::Uint::<N>::parse(input)?;
327 Ok(HasComponent::<Component<N>>::component(input.state())
328 .files
329 .get(u.0)
330 .unwrap()
331 .is_none())
332 }
333}
334
335#[derive(Debug)]
336struct IoError {
337 title: String,
338 underlying_error: std::io::Error,
339}
340
341impl error::TexError for IoError {
342 fn kind(&self) -> error::Kind {
343 error::Kind::FailedPrecondition
344 }
345
346 fn title(&self) -> String {
347 self.title.clone()
348 }
349
350 fn notes(&self) -> Vec<error::display::Note> {
351 vec![format!("underlying filesystem error: {}", self.underlying_error).into()]
352 }
353}
354
355#[derive(Debug)]
356struct UnmatchedBracesError {
357 unmatched_brace: token::Token,
358}
359
360impl error::TexError for UnmatchedBracesError {
361 fn kind(&self) -> error::Kind {
362 error::Kind::Token(self.unmatched_brace)
363 }
364
365 fn title(&self) -> String {
366 "file has an unmatched opening brace".into()
367 }
368
369 fn notes(&self) -> Vec<error::display::Note> {
370 vec![r"files being read with the \read primitive must match all opening braces with closing braces".into()]
371 }
372}
373
374struct To;
376
377impl Parsable for To {
378 fn parse_impl<S: TexlangState>(input: &mut vm::ExpandedStream<S>) -> txl::Result<Self> {
379 texlang::parse::parse_keyword(input, "to")?;
380 Ok(To {})
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::{def, expansion, prefix};
388 use std::collections::HashMap;
389 use texlang_testing::*;
390
391 #[derive(Default)]
392 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
393 struct State {
394 conditional: conditional::Component,
395 input: Component<16>,
396 prefix: prefix::Component,
397 testing: TestingComponent,
398 #[cfg_attr(feature = "serde", serde(skip))]
399 file_system: Rc<RefCell<common::InMemoryFileSystem>>,
400 #[cfg_attr(feature = "serde", serde(skip))]
401 terminal_in: Rc<RefCell<common::MockTerminalIn>>,
402 }
403
404 impl TexlangState for State {}
405
406 impl common::HasFileSystem for State {
407 fn file_system(&self) -> Rc<RefCell<dyn common::FileSystem>> {
408 self.file_system.clone()
409 }
410 }
411
412 impl common::HasTerminalIn for State {
413 fn terminal_in(&self) -> Rc<RefCell<dyn common::TerminalIn>> {
414 self.terminal_in.clone()
415 }
416 }
417
418 implement_has_component![State{
419 conditional: conditional::Component,
420 input: Component<16>,
421 prefix: prefix::Component,
422 testing: TestingComponent,
423 }];
424
425 fn built_in_commands() -> HashMap<&'static str, command::BuiltIn<State>> {
426 HashMap::from([
427 ("closein", get_closein()),
428 ("def", def::get_def()),
429 ("else", conditional::get_else()),
430 ("endinput", get_endinput()),
431 ("fi", conditional::get_fi()),
432 ("ifeof", get_ifeof()),
433 ("input", get_input()),
434 ("openin", get_openin()),
435 ("read", get_read()),
436 ("relax", expansion::get_relax()),
437 ])
438 }
439
440 fn custom_vm_initialization(vm: &mut vm::VM<State>) {
441 let mut fs = common::InMemoryFileSystem::new(&vm.working_directory.as_ref().unwrap());
442 fs.add_string_file("file1.tex", "content1\n");
443 fs.add_string_file("file2.tex", "content2%\n");
444 fs.add_string_file("file3.tex", r"\input nested/file4");
445 fs.add_string_file("nested/file4.tex", "content4");
446 fs.add_string_file("file5.tex", "file1.tex");
447 fs.add_string_file("recursive.tex", r"\input recursive.tex content");
448 vm.state.file_system = Rc::new(RefCell::new(fs));
449 }
450
451 test_suite!(
452 @options(
453 TestOption::BuiltInCommands(built_in_commands),
454 TestOption::CustomVMInitialization(custom_vm_initialization),
455 ),
456 expansion_equality_tests(
457 (basic_case, r"\input file1 hello", "content1 hello"),
458 (input_together, r"\input file2 hello", r"content2hello"),
459 (basic_case_with_ext, r"\input file1.tex", r"content1 "),
460 (nested, r"\input file3", r"content4"),
461 (nested_2, r"\input \input file5", r"content1 "),
462 ),
463 fatal_error_tests(
464 (file_does_not_exist, r"\input doesNotExist"),
465 (recursive_input, r"\input recursive s"),
466 ),
467 );
468
469 fn end_input_vm_initialization(vm: &mut vm::VM<State>) {
470 let mut fs = common::InMemoryFileSystem::new(&vm.working_directory.as_ref().unwrap());
471 fs.add_string_file(
472 "file1.tex",
473 "Hello\\def\\Macro{Hola\\endinput Mundo}\\Macro World\n",
474 );
475 vm.state.file_system = Rc::new(RefCell::new(fs));
476 }
477
478 test_suite!(
479 @option(TestOption::BuiltInCommands(built_in_commands)),
480 @option(TestOption::CustomVMInitialization(end_input_vm_initialization)),
481 expansion_equality_tests(
482 (end_input_simple, r"Hello\endinput World", "Hello",),
483 (
484 end_input_in_second_file,
485 r"Before\input file1 After",
486 "BeforeHelloHolaMundoAfter"
487 ),
488 ),
489 );
490
491 fn read_vm_initialization(vm: &mut vm::VM<State>) {
492 let mut fs = common::InMemoryFileSystem::new(&vm.working_directory.as_ref().unwrap());
493 fs.add_string_file("file1.tex", "1\n2%\n3");
494 fs.add_string_file("file2.tex", "1{\n2\n3}");
495 fs.add_string_file("file3.tex", "1}1\n2");
496 fs.add_string_file("file4.tex", "");
497 fs.add_string_file("file5.tex", "hello { world");
498 vm.state.file_system = Rc::new(RefCell::new(fs));
499
500 let mut terminal_in: common::MockTerminalIn = Default::default();
501 terminal_in.add_line("first-line");
502 terminal_in.add_line("second-line {");
503 terminal_in.add_line("third-line }");
504 terminal_in.add_line("fourth}line");
505 vm.state.terminal_in = Rc::new(RefCell::new(terminal_in));
506 }
507
508 test_suite!(
509 @options(
510 TestOption::BuiltInCommands(built_in_commands),
511 TestOption::CustomVMInitialization(read_vm_initialization),
512 ),
513 expansion_equality_tests(
514 (
515 ifeof_nothing_open,
516 r"\ifeof 0 Closed\else Open\fi",
517 "Closed",
518 ),
519 (
520 ifeof_non_existent_file,
521 r"\openin 0 doesNotExist \ifeof 0 Closed\else Open\fi",
522 "Closed",
523 ),
524 (
525 ifeof_file_exists,
526 r"\openin 0 file1 \ifeof 0 Closed\else Open\fi",
527 "Open",
528 ),
529 (
530 ifeof_non_existent_file_2,
531 r"\openin 0 file1 \openin 0 doesNotExist \ifeof 0 Closed\else Open\fi",
532 "Closed",
533 ),
534 (
535 ifeof_file_closed,
536 r"\openin 0 file1 \closein 0 \ifeof 0 Closed\else Open\fi",
537 "Closed",
538 ),
539 (
540 read_1,
541 r"\openin 0 file1\read 0 to \line line1='\line'\read 0 to \line line2='\line'\read 0 to \line line3='\line'\ifeof 0 Closed\else Open\fi",
542 "line1='1 'line2='2'line3='3 'Closed",
543 ),
544 (
545 read_2,
546 r"\openin 0 file2\read 0 to ~line1='~'\ifeof 0 Closed\else Open\fi",
547 "line1='1{ 2 3} 'Closed",
548 ),
549 (
550 read_3,
551 r"\openin 0 file3\read 0 to \line line1='\line'\read 0 to \line line2='\line'",
552 "line1='1'line2='2 '",
553 ),
554 (
555 read_4,
556 r"\def\par{par}\openin 0 file4\read 0 to \line line1='\line'\ifeof 0 Closed\else Open\fi",
557 "line1='par'Closed",
558 ),
559 (
560 read_from_terminal,
561 r"\read 0 to \line line1='\line'\read 0 to \line line2='\line'\read 0 to \line line3='\line'",
562 "line1='first-line 'line2='second-line { third-line } 'line3='fourth'",
563 ),
564 ),
565 serde_tests((
566 ifeof_file_exists_serde,
567 r"\openin 0 file1 ",
568 r"\ifeof 0 Closed\else Open\fi"
569 ),),
570 fatal_error_tests(
571 (
572 file_has_unmatched_braces,
573 r"\openin 0 file5 \read 0 to \X (\X)",
574 ),
575 (
576 failed_to_read_from_terminal,
577 r"\read 0 to \X \read 0 to \X \read 0 to \X \read 0 to \X",
578 ),
579 ),
580 );
581}