ftml_extraction/open/
mod.rs

1use std::borrow::Cow;
2
3use either::Either;
4use flams_ontology::{
5    content::{
6        declarations::{
7            morphisms::Morphism,
8            structures::{Extension, MathStructure},
9            symbols::{ArgSpec, AssocType, Symbol},
10            OpenDeclaration,
11        },
12        modules::{NestedModule, OpenModule},
13        terms::{Term, Var},
14    },
15    languages::Language,
16    narration::{
17        notations::Notation,
18        paragraphs::{LogicalParagraph, ParagraphFormatting, ParagraphKind},
19        problems::{
20            ChoiceBlock, FillInSol, FillInSolOption, GradingNote, Problem, SolutionData, Solutions,
21        },
22        sections::{Section, SectionLevel},
23        variables::Variable,
24        DocumentElement,
25    },
26    uris::{
27        ContentURI, DocumentElementURI, DocumentURI, ModuleURI, Name, SymbolURI, URIOrRefTrait,
28    },
29};
30use smallvec::SmallVec;
31use terms::{OpenArg, PreVar, VarOrSym};
32
33#[cfg(feature = "rdf")]
34use flams_ontology::triple;
35
36use crate::{
37    errors::FTMLError,
38    prelude::{FTMLExtractor, FTMLNode, NotationState, ParagraphState, ProblemState},
39    rules::FTMLElements,
40};
41
42pub mod terms;
43#[allow(clippy::large_enum_variant)]
44#[derive(Debug, Clone)]
45pub enum OpenFTMLElement {
46    Invisible,
47    SetSectionLevel(SectionLevel),
48    ImportModule(ModuleURI),
49    UseModule(ModuleURI),
50    Slide(DocumentElementURI),
51    SlideNumber,
52    ProofBody,
53    Module {
54        uri: ModuleURI,
55        meta: Option<ModuleURI>,
56        signature: Option<Language>,
57    },
58    MathStructure {
59        uri: SymbolURI,
60        macroname: Option<Box<str>>,
61    },
62    Morphism {
63        uri: SymbolURI,
64        domain: ModuleURI,
65        total: bool,
66    },
67    Assign(SymbolURI),
68    Section {
69        lvl: SectionLevel,
70        uri: DocumentElementURI,
71    },
72    SkipSection,
73    Paragraph {
74        uri: DocumentElementURI,
75        kind: ParagraphKind,
76        formatting: ParagraphFormatting,
77        styles: Box<[Name]>,
78    },
79    Problem {
80        uri: DocumentElementURI,
81        styles: Box<[Name]>,
82        autogradable: bool,
83        points: Option<f32>,
84        sub_problem: bool,
85    },
86    Doctitle,
87    Title,
88    ProofTitle,
89    SubproofTitle,
90    Symdecl {
91        uri: SymbolURI,
92        arity: ArgSpec,
93        macroname: Option<Box<str>>,
94        role: Box<[Box<str>]>,
95        assoctype: Option<AssocType>,
96        reordering: Option<Box<str>>,
97    },
98    Vardecl {
99        uri: DocumentElementURI,
100        arity: ArgSpec,
101        bind: bool,
102        macroname: Option<Box<str>>,
103        role: Box<[Box<str>]>,
104        assoctype: Option<AssocType>,
105        reordering: Option<Box<str>>,
106        is_seq: bool,
107    },
108    Notation {
109        id: Box<str>,
110        symbol: VarOrSym,
111        precedence: isize,
112        argprecs: SmallVec<isize, 9>,
113    },
114    NotationComp,
115    NotationOpComp,
116    Definiendum(SymbolURI),
117    Type,
118    Conclusion {
119        uri: SymbolURI,
120        in_term: bool,
121    },
122    Definiens {
123        uri: Option<SymbolURI>,
124        in_term: bool,
125    },
126    OpenTerm {
127        term: terms::OpenTerm,
128        is_top: bool,
129    },
130    ClosedTerm(Term),
131    MMTRule(Box<str>),
132    ArgSep,
133    ArgMap,
134    ArgMapSep,
135    HeadTerm,
136    ProblemHint,
137    ProblemSolution(Option<Box<str>>),
138    ProblemGradingNote,
139    AnswerClass,
140    AnswerClassFeedback,
141    ChoiceBlock {
142        multiple: bool,
143        inline: bool,
144    },
145    ProblemChoice,
146    ProblemChoiceVerdict,
147    ProblemChoiceFeedback,
148    Fillinsol(Option<f32>),
149    FillinsolCase,
150
151    Inputref {
152        uri: DocumentURI,
153        id: Box<str>,
154    },
155    IfInputref(bool),
156
157    Comp,
158    MainComp,
159    DefComp,
160    Arg(OpenArg),
161}
162
163impl OpenFTMLElement {
164    #[allow(clippy::too_many_lines)]
165    #[allow(clippy::cognitive_complexity)]
166    pub(crate) fn close<E: FTMLExtractor, N: FTMLNode>(
167        self,
168        previous: &mut FTMLElements,
169        next: &mut FTMLElements,
170        extractor: &mut E,
171        node: &N,
172    ) -> Option<Self> {
173        //println!("{self:?}}}");
174        match self {
175            Self::Invisible => {
176                if !extractor.in_term() && !extractor.in_notation() {
177                    node.delete();
178                }
179            }
180            Self::SetSectionLevel(lvl) => {
181                extractor.add_document_element(DocumentElement::SetSectionLevel(lvl))
182            }
183            Self::ImportModule(uri) => Self::close_importmodule(extractor, uri),
184            Self::UseModule(uri) => Self::close_usemodule(extractor, uri),
185            Self::Module {
186                uri,
187                meta,
188                signature,
189            } => Self::close_module(extractor, node, uri, meta, signature),
190            Self::MathStructure { uri, macroname } => {
191                Self::close_structure(extractor, node, uri, macroname)
192            }
193            Self::Morphism { uri, domain, total } => {
194                Self::close_morphism(extractor, node, uri, domain, total)
195            }
196
197            Self::Assign(_sym) => {
198                if extractor.close_complex_term().is_some() {}
199                // TODO
200            }
201            Self::SkipSection => {
202                if let Some((_, _, children)) = extractor.close_section() {
203                    extractor.add_document_element(DocumentElement::SkipSection(children));
204                } else {
205                    extractor.add_error(FTMLError::NotInNarrative);
206                };
207            }
208
209            Self::Section { lvl, uri } => Self::close_section(extractor, node, lvl, uri),
210            Self::Slide(uri) => {
211                if let Some(children) = extractor.close_slide() {
212                    extractor.add_document_element(DocumentElement::Slide {
213                        range: node.range(),
214                        uri,
215                        children,
216                    });
217                } else {
218                    extractor.add_error(FTMLError::NotInNarrative);
219                };
220            }
221            Self::Paragraph {
222                kind,
223                formatting,
224                styles,
225                uri,
226            } => Self::close_paragraph(extractor, node, kind, formatting, styles, uri),
227            Self::Problem {
228                uri,
229                styles,
230                autogradable,
231                points,
232                sub_problem,
233            } => Self::close_problem(
234                extractor,
235                node,
236                uri,
237                styles,
238                autogradable,
239                points,
240                sub_problem,
241            ),
242
243            Self::Doctitle => {
244                extractor.set_document_title(node.inner_string().into_boxed_str());
245            }
246
247            Self::Title => {
248                if extractor.add_title(node.inner_range()).is_err() {
249                    extractor.add_error(FTMLError::NotInNarrative);
250                }
251            }
252            Self::Symdecl {
253                uri,
254                arity,
255                macroname,
256                role,
257                assoctype,
258                reordering,
259            } => Self::close_symdecl(
260                extractor, uri, arity, macroname, role, assoctype, reordering,
261            ),
262            Self::Vardecl {
263                uri,
264                arity,
265                bind,
266                macroname,
267                role,
268                assoctype,
269                reordering,
270                is_seq,
271            } => Self::close_vardecl(
272                extractor, uri, bind, arity, macroname, role, assoctype, reordering, is_seq,
273            ),
274            Self::Notation {
275                id,
276                symbol,
277                precedence,
278                argprecs,
279            } => Self::close_notation(extractor, id, symbol, precedence, argprecs),
280            Self::NotationComp => {
281                if let Some(n) = node.as_notation() {
282                    if extractor.add_notation(n).is_err() {
283                        extractor.add_error(FTMLError::NotInNarrative);
284                    }
285                } else {
286                    extractor.add_error(FTMLError::NotInNarrative);
287                }
288            }
289            Self::NotationOpComp => {
290                if let Some(n) = node.as_op_notation() {
291                    if extractor.add_op_notation(n).is_err() {
292                        extractor.add_error(FTMLError::NotInNarrative);
293                    }
294                } else {
295                    extractor.add_error(FTMLError::NotInNarrative);
296                }
297            }
298            Self::Type => {
299                extractor.set_in_term(false);
300                let tm = Self::as_term(next, node);
301                if extractor.add_type(tm).is_err() {
302                    extractor.add_error(FTMLError::NotInContent);
303                }
304            }
305            Self::Conclusion { uri, in_term } => {
306                extractor.set_in_term(in_term);
307                let tm = Self::as_term(next, node);
308                if extractor.add_term(Some(uri), tm).is_err() {
309                    extractor.add_error(FTMLError::NotInContent);
310                }
311            }
312            Self::Definiens { uri, in_term } => {
313                extractor.set_in_term(in_term);
314                let tm = Self::as_term(next, node);
315                if extractor.add_term(uri, tm).is_err() {
316                    extractor.add_error(FTMLError::NotInContent);
317                }
318            }
319            Self::OpenTerm { term, is_top: true } => {
320                let term = term.close(extractor);
321                let uri = match extractor.get_narrative_uri()
322                    & &*extractor.new_id(Cow::Borrowed("term"))
323                {
324                    Ok(uri) => uri,
325                    Err(_) => {
326                        extractor
327                            .add_error(FTMLError::InvalidURI("(should be impossible)".to_string()));
328                        return None;
329                    }
330                };
331                extractor.set_in_term(false);
332                if !matches!(term, Term::OMID { .. } | Term::OMV { .. }) {
333                    extractor.add_document_element(DocumentElement::TopTerm { uri, term });
334                }
335            }
336            Self::OpenTerm {
337                term,
338                is_top: false,
339            } => {
340                let term = term.close(extractor);
341                return Some(Self::ClosedTerm(term));
342            }
343            Self::MMTRule(_id) => {
344                let _ = extractor.close_args();
345                // TODO
346            }
347            Self::ArgSep => {
348                return Some(Self::ArgSep);
349            }
350            Self::ArgMap => {
351                return Some(Self::ArgMap);
352            }
353            Self::ArgMapSep => {
354                return Some(Self::ArgMapSep);
355            }
356            Self::Arg(a) => {
357                if extractor.in_notation() {
358                    return Some(self);
359                }
360                let t = node.as_term();
361                let pos = match a.index {
362                    Either::Left(u) => (u, None),
363                    Either::Right((a, b)) => (a, Some(b)),
364                };
365                if extractor.add_arg(pos, t, a.mode).is_err() {
366                    //println!("HERE 1");
367                    extractor.add_error(FTMLError::IncompleteArgs(3));
368                }
369            }
370            Self::HeadTerm => {
371                let tm = node.as_term();
372                if extractor.add_term(None, tm).is_err() {
373                    //println!("HERE 2");
374                    extractor.add_error(FTMLError::IncompleteArgs(4));
375                }
376            }
377
378            Self::Comp | Self::MainComp if extractor.in_notation() => {
379                return Some(self);
380            }
381            Self::DefComp if extractor.in_notation() => {
382                return Some(Self::Comp);
383            }
384            Self::ClosedTerm(_) => return Some(self),
385
386            Self::Inputref { uri, id } => {
387                let top = extractor.get_narrative_uri();
388                #[cfg(feature = "rdf")]
389                if E::RDF {
390                    extractor.add_triples([triple!(<(top.to_iri())> dc:HAS_PART <(uri.to_iri())>)]);
391                }
392                extractor.add_document_element(DocumentElement::DocumentReference {
393                    id: match top & &*id {
394                        Ok(id) => id,
395                        Err(_) => {
396                            extractor.add_error(FTMLError::InvalidURI(format!("5: {id}")));
397                            return None;
398                        }
399                    },
400                    range: node.range(),
401                    target: uri,
402                });
403                previous.elems.retain(|e| !matches!(e, Self::Invisible));
404            }
405            Self::ProblemHint => {
406                if extractor
407                    .with_problem(|ex| ex.hints.push(node.inner_range()))
408                    .is_none()
409                {
410                    extractor.add_error(FTMLError::NotInProblem("a"));
411                }
412            }
413            Self::ProblemSolution(id) => {
414                let range = node.range();
415                // kinda-hack: remove all paragraphs in a solution
416                extractor.with_problem(|ex| {
417                    ex.children.retain(|e| match e {
418                        DocumentElement::Paragraph(LogicalParagraph { range: rng, .. })
419                        | DocumentElement::Slide { range: rng, .. }
420                        | DocumentElement::Problem(Problem { range: rng, .. }) => {
421                            rng.end < range.start || rng.start > range.end
422                        }
423
424                        _ => true,
425                    });
426                });
427                let s = node.inner_string().into_boxed_str();
428                node.delete_children();
429                if extractor
430                    .with_problem(|ex| {
431                        ex.solutions.push(SolutionData::Solution {
432                            html: s,
433                            answer_class: id,
434                        });
435                    })
436                    .is_none()
437                {
438                    extractor.add_error(FTMLError::NotInProblem("b"));
439                }
440            }
441            Self::ProblemGradingNote => {
442                let s = node.inner_string().into_boxed_str();
443                node.delete_children();
444                if let Some(gnote) = extractor.close_gnote() {
445                    let gnote = GradingNote {
446                        answer_classes: gnote.answer_classes,
447                        html: s,
448                    };
449                    let r = extractor.add_resource(&gnote);
450                    if extractor.with_problem(|ex| ex.gnotes.push(r)).is_none() {
451                        extractor.add_error(FTMLError::NotInProblem("c"));
452                    }
453                } else {
454                    extractor.add_error(FTMLError::NotInProblem("d"));
455                }
456            }
457            Self::ChoiceBlock { .. } => {
458                let range = node.range();
459                if let Some(cb) = extractor.close_choice_block() {
460                    if extractor
461                        .with_problem(|ex| {
462                            ex.solutions.push(SolutionData::ChoiceBlock(ChoiceBlock {
463                                multiple: cb.multiple,
464                                inline: cb.inline,
465                                range,
466                                styles: cb.styles,
467                                choices: cb.choices,
468                            }))
469                        })
470                        .is_none()
471                    {
472                        extractor.add_error(FTMLError::NotInProblem("e"));
473                    }
474                } else {
475                    extractor.add_error(FTMLError::NotInProblem("f"));
476                }
477            }
478            Self::AnswerClassFeedback => {
479                let s = node.string().into_boxed_str();
480                node.delete();
481                if !extractor
482                    .with_problem(|ex| {
483                        if let Some(n) = &mut ex.gnote {
484                            if let Some(ac) = n.answer_classes.last_mut() {
485                                ac.feedback = s;
486                                true
487                            } else {
488                                false
489                            }
490                        } else {
491                            false
492                        }
493                    })
494                    .unwrap_or_default()
495                {
496                    extractor.add_error(FTMLError::NotInProblem("g"));
497                }
498            }
499            Self::ProblemChoiceVerdict => {
500                let s = node.string().into_boxed_str();
501                node.delete();
502                if !extractor
503                    .with_problem(|ex| {
504                        if let Some(n) = &mut ex.choice_block {
505                            if let Some(ac) = n.choices.last_mut() {
506                                ac.verdict = s;
507                                true
508                            } else {
509                                false
510                            }
511                        } else {
512                            false
513                        }
514                    })
515                    .unwrap_or_default()
516                {
517                    extractor.add_error(FTMLError::NotInProblem("h"));
518                }
519            }
520            Self::ProblemChoiceFeedback => {
521                let s = node.string().into_boxed_str();
522                node.delete();
523                if !extractor
524                    .with_problem(|ex| {
525                        if let Some(n) = &mut ex.choice_block {
526                            if let Some(ac) = n.choices.last_mut() {
527                                ac.feedback = s;
528                                true
529                            } else {
530                                false
531                            }
532                        } else {
533                            false
534                        }
535                    })
536                    .unwrap_or_default()
537                {
538                    extractor.add_error(FTMLError::NotInProblem("i"));
539                }
540            }
541            Self::Fillinsol(width) => {
542                if !extractor
543                    .with_problem(|ex| {
544                        if let Some(n) = std::mem::take(&mut ex.fillinsol) {
545                            ex.solutions.push(SolutionData::FillInSol(FillInSol {
546                                width,
547                                opts: n.cases,
548                            }));
549                            true
550                        } else {
551                            false
552                        }
553                    })
554                    .unwrap_or_default()
555                {
556                    extractor.add_error(FTMLError::NotInProblem("j"));
557                }
558                node.delete_children();
559            }
560            Self::FillinsolCase => {
561                let s = node.inner_string().into_boxed_str();
562                node.delete();
563                if !extractor
564                    .with_problem(|ex| {
565                        if let Some(n) = &mut ex.fillinsol {
566                            n.cases.last_mut().is_some_and(|n| match n {
567                                FillInSolOption::Exact { feedback, .. }
568                                | FillInSolOption::NumericalRange { feedback, .. }
569                                | FillInSolOption::Regex { feedback, .. } => {
570                                    *feedback = s;
571                                    true
572                                }
573                            })
574                        } else {
575                            false
576                        }
577                    })
578                    .unwrap_or_default()
579                {
580                    extractor.add_error(FTMLError::NotInProblem("k"));
581                }
582            }
583            Self::IfInputref(_)
584            | Self::Definiendum(_)
585            | Self::Comp
586            | Self::MainComp
587            | Self::DefComp
588            | Self::AnswerClass
589            | Self::ProblemChoice
590            | Self::SlideNumber
591            | Self::ProofBody
592            | Self::ProofTitle
593            | Self::SubproofTitle => (),
594        }
595        None
596    }
597
598    fn as_term<N: FTMLNode>(next: &mut FTMLElements, node: &N) -> Term {
599        if let Some(i) = next.iter().position(|e| matches!(e, Self::ClosedTerm(_))) {
600            let Self::ClosedTerm(t) = next.elems.remove(i) else {
601                unreachable!()
602            };
603            return t;
604        }
605        node.as_term()
606    }
607
608    fn close_importmodule<E: FTMLExtractor>(extractor: &mut E, uri: ModuleURI) {
609        #[cfg(feature = "rdf")]
610        if E::RDF {
611            if let Some(m) = extractor.get_content_iri() {
612                extractor.add_triples([triple!(<(m)> ulo:IMPORTS <(uri.to_iri())>)]);
613            }
614        }
615        extractor.add_document_element(DocumentElement::ImportModule(uri.clone()));
616        if extractor
617            .add_content_element(OpenDeclaration::Import(uri))
618            .is_err()
619        {
620            extractor.add_error(FTMLError::NotInContent);
621        }
622    }
623
624    fn close_usemodule<E: FTMLExtractor>(extractor: &mut E, uri: ModuleURI) {
625        #[cfg(feature = "rdf")]
626        if E::RDF {
627            extractor.add_triples([
628                triple!(<(extractor.get_document_iri())> dc:REQUIRES <(uri.to_iri())>),
629            ]);
630        }
631        extractor.add_document_element(DocumentElement::UseModule(uri));
632    }
633
634    fn close_module<E: FTMLExtractor, N: FTMLNode>(
635        extractor: &mut E,
636        node: &N,
637        uri: ModuleURI,
638        meta: Option<ModuleURI>,
639        signature: Option<Language>,
640    ) {
641        let Some((_, narrative)) = extractor.close_narrative() else {
642            extractor.add_error(FTMLError::NotInNarrative);
643            return;
644        };
645        let Some((_, mut content)) = extractor.close_content() else {
646            extractor.add_error(FTMLError::NotInContent);
647            return;
648        };
649
650        #[cfg(feature = "rdf")]
651        if E::RDF {
652            let iri = uri.to_iri();
653            extractor.add_triples([
654                triple!(<(iri.clone())> : ulo:THEORY),
655                triple!(<(extractor.get_document_iri())> ulo:CONTAINS <(iri)>),
656            ]);
657        }
658
659        extractor.add_document_element(DocumentElement::Module {
660            range: node.range(),
661            module: uri.clone(),
662            children: narrative,
663        });
664
665        if uri.name().is_simple() {
666            extractor.add_module(OpenModule {
667                uri,
668                meta,
669                signature,
670                elements: content,
671            });
672        } else {
673            // NestedModule
674            let Some(sym) = uri.into_symbol() else {
675                unreachable!()
676            };
677            #[cfg(feature = "rdf")]
678            if E::RDF {
679                if let Some(m) = extractor.get_content_iri() {
680                    extractor.add_triples([triple!(<(m)> ulo:CONTAINS <(sym.to_iri())>)]);
681                }
682            }
683            if extractor
684                .add_content_element(OpenDeclaration::NestedModule(NestedModule {
685                    uri: sym,
686                    elements: std::mem::take(&mut content),
687                }))
688                .is_err()
689            {
690                extractor.add_error(FTMLError::NotInContent);
691            }
692        }
693    }
694
695    fn close_structure<E: FTMLExtractor, N: FTMLNode>(
696        extractor: &mut E,
697        node: &N,
698        uri: SymbolURI,
699        macroname: Option<Box<str>>,
700    ) {
701        let Some((_, narrative)) = extractor.close_narrative() else {
702            extractor.add_error(FTMLError::NotInNarrative);
703            return;
704        };
705        let Some((_, content)) = extractor.close_content() else {
706            extractor.add_error(FTMLError::NotInContent);
707            return;
708        };
709
710        #[cfg(feature = "rdf")]
711        if E::RDF {
712            if let Some(cont) = extractor.get_content_iri() {
713                let iri = uri.to_iri();
714                extractor.add_triples([
715                    triple!(<(iri.clone())> : ulo:STRUCTURE),
716                    triple!(<(cont)> ulo:CONTAINS <(iri)>),
717                ]);
718            }
719        }
720
721        if uri.name().last_name().as_ref().starts_with("EXTSTRUCT") {
722            let Some(target) = content.iter().find_map(|d| match d {
723                OpenDeclaration::Import(uri)
724                    if !uri.name().last_name().as_ref().starts_with("EXTSTRUCT") =>
725                {
726                    Some(uri)
727                }
728                _ => None,
729            }) else {
730                extractor.add_error(FTMLError::NotInContent);
731                return;
732            };
733            let Some(target) = target.clone().into_symbol() else {
734                extractor.add_error(FTMLError::NotInContent);
735                return;
736            };
737
738            #[cfg(feature = "rdf")]
739            if E::RDF {
740                extractor.add_triples([triple!(<(uri.to_iri())> ulo:EXTENDS <(target.to_iri())>)]);
741            }
742            extractor.add_document_element(DocumentElement::Extension {
743                range: node.range(),
744                extension: uri.clone(),
745                target: target.clone(),
746                children: narrative,
747            });
748            if extractor
749                .add_content_element(OpenDeclaration::Extension(Extension {
750                    uri,
751                    elements: content,
752                    target,
753                }))
754                .is_err()
755            {
756                extractor.add_error(FTMLError::NotInContent);
757            }
758        } else {
759            extractor.add_document_element(DocumentElement::MathStructure {
760                range: node.range(),
761                structure: uri.clone(),
762                children: narrative,
763            });
764            if extractor
765                .add_content_element(OpenDeclaration::MathStructure(MathStructure {
766                    uri,
767                    elements: content,
768                    macroname,
769                }))
770                .is_err()
771            {
772                extractor.add_error(FTMLError::NotInContent);
773            }
774        }
775    }
776
777    fn close_morphism<E: FTMLExtractor, N: FTMLNode>(
778        extractor: &mut E,
779        node: &N,
780        uri: SymbolURI,
781        domain: ModuleURI,
782        total: bool,
783    ) {
784        let Some((_, narrative)) = extractor.close_narrative() else {
785            extractor.add_error(FTMLError::NotInNarrative);
786            return;
787        };
788        let Some((_, content)) = extractor.close_content() else {
789            extractor.add_error(FTMLError::NotInContent);
790            return;
791        };
792
793        #[cfg(feature = "rdf")]
794        if E::RDF {
795            if let Some(cont) = extractor.get_content_iri() {
796                let iri = uri.to_iri(); // TODO
797                extractor.add_triples([
798                    triple!(<(iri.clone())> : ulo:MORPHISM),
799                    triple!(<(iri.clone())> rdfs:DOMAIN <(domain.to_iri())>),
800                    triple!(<(cont)> ulo:CONTAINS <(iri)>),
801                ]);
802            }
803        }
804
805        extractor.add_document_element(DocumentElement::Morphism {
806            range: node.range(),
807            morphism: uri.clone(),
808            children: narrative,
809        });
810        if extractor
811            .add_content_element(OpenDeclaration::Morphism(Morphism {
812                uri,
813                domain,
814                total,
815                elements: content,
816            }))
817            .is_err()
818        {
819            extractor.add_error(FTMLError::NotInContent);
820        }
821    }
822
823    fn close_section<E: FTMLExtractor, N: FTMLNode>(
824        extractor: &mut E,
825        node: &N,
826        lvl: SectionLevel,
827        uri: DocumentElementURI,
828    ) {
829        let Some((_, title, children)) = extractor.close_section() else {
830            extractor.add_error(FTMLError::NotInNarrative);
831            return;
832        };
833
834        #[cfg(feature = "rdf")]
835        if E::RDF {
836            let doc = extractor.get_document_iri();
837            let iri = uri.to_iri();
838            extractor.add_triples([
839                triple!(<(iri.clone())> : ulo:SECTION),
840                triple!(<(doc)> ulo:CONTAINS <(iri)>),
841            ]);
842        }
843
844        extractor.add_document_element(DocumentElement::Section(Section {
845            range: node.range(),
846            level: lvl,
847            title,
848            uri,
849            children,
850        }));
851    }
852
853    fn close_paragraph<E: FTMLExtractor, N: FTMLNode>(
854        extractor: &mut E,
855        node: &N,
856        kind: ParagraphKind,
857        formatting: ParagraphFormatting,
858        styles: Box<[Name]>,
859        uri: DocumentElementURI,
860    ) {
861        let Some(ParagraphState {
862            children,
863            fors,
864            title,
865            ..
866        }) = extractor.close_paragraph()
867        else {
868            extractor.add_error(FTMLError::NotInParagraph);
869            return;
870        };
871
872        #[cfg(feature = "rdf")]
873        if E::RDF {
874            let doc = extractor.get_document_iri();
875            let iri = uri.to_iri();
876            if kind.is_definition_like(&styles) {
877                for (f, _) in fors.iter() {
878                    extractor.add_triples([triple!(<(iri.clone())> ulo:DEFINES <(f.to_iri())>)]);
879                }
880            } else if kind == ParagraphKind::Example {
881                for (f, _) in fors.iter() {
882                    extractor
883                        .add_triples([triple!(<(iri.clone())> ulo:EXAMPLE_FOR <(f.to_iri())>)]);
884                }
885            }
886            extractor.add_triples([
887                triple!(<(iri.clone())> : <(kind.rdf_type().into_owned())>),
888                triple!(<(doc)> ulo:CONTAINS <(iri)>),
889            ]);
890        }
891
892        extractor.add_document_element(DocumentElement::Paragraph(LogicalParagraph {
893            range: node.range(),
894            kind,
895            formatting,
896            styles,
897            fors,
898            uri,
899            children,
900            title,
901        }));
902    }
903
904    fn close_problem<E: FTMLExtractor, N: FTMLNode>(
905        extractor: &mut E,
906        node: &N,
907        uri: DocumentElementURI,
908        styles: Box<[Name]>,
909        autogradable: bool,
910        points: Option<f32>,
911        sub_problem: bool,
912    ) {
913        let Some(ProblemState {
914            solutions,
915            hints,
916            notes,
917            gnotes,
918            title,
919            children,
920            preconditions,
921            objectives,
922            ..
923        }) = extractor.close_problem()
924        else {
925            extractor.add_error(FTMLError::NotInProblem("l"));
926            return;
927        };
928
929        #[cfg(feature = "rdf")]
930        if E::RDF {
931            let doc = extractor.get_document_iri();
932            let iri = uri.to_iri();
933            for (d, s) in &preconditions {
934                let b = flams_ontology::rdf::BlankNode::default();
935                extractor.add_triples([
936                    triple!(<(iri.clone())> ulo:PRECONDITION (b.clone())!),
937                    triple!((b.clone())! ulo:COGDIM <(d.to_iri().into_owned())>),
938                    triple!((b)! ulo:POSYMBOL <(s.to_iri())>),
939                ]);
940            }
941            for (d, s) in &objectives {
942                let b = flams_ontology::rdf::BlankNode::default();
943                extractor.add_triples([
944                    triple!(<(iri.clone())> ulo:OBJECTIVE (b.clone())!),
945                    triple!((b.clone())! ulo:COGDIM <(d.to_iri().into_owned())>),
946                    triple!((b)! ulo:POSYMBOL <(s.to_iri())>),
947                ]);
948            }
949
950            extractor.add_triples([
951                if sub_problem {
952                    triple!(<(iri.clone())> : ulo:SUBPROBLEM)
953                } else {
954                    triple!(<(iri.clone())> : ulo:PROBLEM)
955                },
956                triple!(<(doc)> ulo:CONTAINS <(iri)>),
957            ]);
958        }
959        let solutions =
960            extractor.add_resource(&Solutions::from_solutions(solutions.into_boxed_slice()));
961
962        extractor.add_document_element(DocumentElement::Problem(Problem {
963            range: node.range(),
964            uri,
965            styles,
966            autogradable,
967            points,
968            sub_problem,
969            gnotes,
970            solutions,
971            hints,
972            notes,
973            title,
974            children,
975            preconditions,
976            objectives,
977        }));
978    }
979
980    fn close_symdecl<E: FTMLExtractor>(
981        extractor: &mut E,
982        uri: SymbolURI,
983        arity: ArgSpec,
984        macroname: Option<Box<str>>,
985        role: Box<[Box<str>]>,
986        assoctype: Option<AssocType>,
987        reordering: Option<Box<str>>,
988    ) {
989        let Some((tp, df)) = extractor.close_decl() else {
990            extractor.add_error(FTMLError::NotInContent);
991            return;
992        };
993        #[cfg(feature = "rdf")]
994        if E::RDF {
995            if let Some(m) = extractor.get_content_iri() {
996                let iri = uri.to_iri();
997                extractor.add_triples([
998                    triple!(<(iri.clone())> : ulo:DECLARATION),
999                    triple!(<(m)> ulo:DECLARES <(iri)>),
1000                ]);
1001            }
1002        }
1003        extractor.add_document_element(DocumentElement::SymbolDeclaration(uri.clone()));
1004        if extractor
1005            .add_content_element(OpenDeclaration::Symbol(Symbol {
1006                uri,
1007                arity,
1008                macroname,
1009                role,
1010                tp,
1011                df,
1012                assoctype,
1013                reordering,
1014            }))
1015            .is_err()
1016        {
1017            extractor.add_error(FTMLError::NotInContent);
1018        }
1019    }
1020
1021    #[allow(clippy::too_many_arguments)]
1022    fn close_vardecl<E: FTMLExtractor>(
1023        extractor: &mut E,
1024        uri: DocumentElementURI,
1025        bind: bool,
1026        arity: ArgSpec,
1027        macroname: Option<Box<str>>,
1028        role: Box<[Box<str>]>,
1029        assoctype: Option<AssocType>,
1030        reordering: Option<Box<str>>,
1031        is_seq: bool,
1032    ) {
1033        let Some((tp, df)) = extractor.close_decl() else {
1034            extractor.add_error(FTMLError::NotInContent);
1035            return;
1036        };
1037
1038        #[cfg(feature = "rdf")]
1039        if E::RDF {
1040            let iri = uri.to_iri();
1041            extractor.add_triples([
1042                triple!(<(iri.clone())> : ulo:VARIABLE),
1043                triple!(<(extractor.get_document_iri())> ulo:DECLARES <(iri)>),
1044            ]);
1045        }
1046
1047        extractor.add_document_element(DocumentElement::Variable(Variable {
1048            uri,
1049            arity,
1050            macroname,
1051            bind,
1052            role,
1053            tp,
1054            df,
1055            assoctype,
1056            reordering,
1057            is_seq,
1058        }));
1059    }
1060
1061    fn close_notation<E: FTMLExtractor>(
1062        extractor: &mut E,
1063        id: Box<str>,
1064        symbol: VarOrSym,
1065        precedence: isize,
1066        argprecs: SmallVec<isize, 9>,
1067    ) {
1068        let Some(NotationState {
1069            attribute_index,
1070            inner_index,
1071            is_text,
1072            components,
1073            op,
1074        }) = extractor.close_notation()
1075        else {
1076            extractor.add_error(FTMLError::NotInNarrative);
1077            return;
1078        };
1079        if attribute_index == 0 {
1080            extractor.add_error(FTMLError::NotInNarrative);
1081            return;
1082        }
1083        let uri = match extractor.get_narrative_uri() & &*id {
1084            Ok(uri) => uri,
1085            Err(_) => {
1086                extractor.add_error(FTMLError::InvalidURI(format!("6: {id}")));
1087                return;
1088            }
1089        };
1090        let notation = extractor.add_resource(&Notation {
1091            attribute_index,
1092            is_text,
1093            inner_index,
1094            components,
1095            op,
1096            precedence,
1097            id,
1098            argprecs,
1099        });
1100        match symbol {
1101            VarOrSym::S(ContentURI::Symbol(symbol)) => {
1102                #[cfg(feature = "rdf")]
1103                if E::RDF {
1104                    let iri = uri.to_iri();
1105                    extractor.add_triples([
1106                        triple!(<(iri.clone())> : ulo:NOTATION),
1107                        triple!(<(iri.clone())> ulo:NOTATION_FOR <(symbol.to_iri())>),
1108                        triple!(<(extractor.get_document_iri())> ulo:DECLARES <(iri)>),
1109                    ]);
1110                }
1111                extractor.add_document_element(DocumentElement::Notation {
1112                    symbol,
1113                    id: uri,
1114                    notation,
1115                });
1116            }
1117            VarOrSym::S(_) => unreachable!(),
1118            VarOrSym::V(PreVar::Resolved(variable)) => {
1119                extractor.add_document_element(DocumentElement::VariableNotation {
1120                    variable,
1121                    id: uri,
1122                    notation,
1123                })
1124            }
1125            VarOrSym::V(PreVar::Unresolved(name)) => match extractor.resolve_variable_name(name) {
1126                Var::Name(name) => extractor.add_error(FTMLError::UnresolvedVariable(name)),
1127                Var::Ref { declaration, .. } => {
1128                    extractor.add_document_element(DocumentElement::VariableNotation {
1129                        variable: declaration,
1130                        id: uri,
1131                        notation,
1132                    })
1133                }
1134            },
1135        }
1136    }
1137}