targetArtifactTypes;
/**
- * Create an instance of {@link MarkdownForwardingSpecificationItem}
+ * Create an instance of {@link ForwardingSpecificationItem}
*
* @param forward
* the textual representation
*/
- public MarkdownForwardingSpecificationItem(final String forward)
+ public ForwardingSpecificationItem(final String forward)
{
final int posForwardMarker = forward.indexOf(FORWARD_MARKER);
final int posOriginalMarker = forward.indexOf(ORIGINAL_MARKER);
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/LightWeightMarkupImporter.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/LightWeightMarkupImporter.java
new file mode 100644
index 000000000..41bc69674
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/LightWeightMarkupImporter.java
@@ -0,0 +1,282 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup;
+
+import org.itsallcode.openfasttrace.api.core.ItemStatus;
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
+import org.itsallcode.openfasttrace.api.importer.Importer;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader.*;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*;
+
+/**
+ * Base class for importers of lightweight markup text.
+ */
+public abstract class LightWeightMarkupImporter implements Importer, LineReaderCallback
+{
+ /** File to be imported */
+ protected final InputFile file;
+ /** Listener for import events */
+ protected final ImportEventListener listener;
+ /** State machine for a line-by-line parser */
+ protected final LineParserStateMachine stateMachine;
+ private String lastTitle;
+ private boolean inSpecificationItem;
+ private LineContext currentContext;
+
+ /**
+ * Create a new {@link LightWeightMarkupImporter}.
+ *
+ * @param file
+ * input file
+ * @param listener
+ * import event listener
+ */
+ // Possible 'this' escape before subclass is fully initialized:
+ // LineParserStateMachine constructor does not use 'this'.
+ @SuppressWarnings("this-escape")
+ protected LightWeightMarkupImporter(final InputFile file, final ImportEventListener listener)
+ {
+ this.file = file;
+ this.listener = listener;
+ this.stateMachine = new LineParserStateMachine(configureTransitions());
+ }
+
+ @Override
+ public void runImport()
+ {
+ new LineReader(file, this).readFile();
+ }
+
+ /**
+ * Define the transitions of the parser statemachine.
+ *
+ * @return parser statemachine transitions
+ */
+ protected abstract Transition[] configureTransitions();
+
+ @Override
+ public void nextLine(final LineContext context)
+ {
+ this.currentContext = context;
+ this.stateMachine.step(this.currentContext.currentLine(), this.currentContext.nextLine());
+ }
+
+ /**
+ * Define a transition in the parser statemachine.
+ *
+ * @param from
+ * state to be matched against the parsers current state
+ * @param to
+ * state the parser will be in if the transition happened
+ * @param pattern
+ * line pattern to be matched for this transition to happen
+ * @param action
+ * action to take as during the transition
+ * @return transition definition
+ */
+ protected static Transition transition(final LineParserState from, final LineParserState to,
+ final LinePattern pattern, final TransitionAction action)
+ {
+ return new Transition(from, to, pattern, action);
+ }
+
+ @Override
+ public void finishReading()
+ {
+ if (this.inSpecificationItem)
+ {
+ this.listener.endSpecificationItem();
+ }
+ }
+
+ /**
+ * Start a new specification item.
+ */
+ protected void beginItem()
+ {
+ cleanUpLastItem();
+ this.inSpecificationItem = true;
+ informListenerAboutNewItem();
+ }
+
+ /**
+ * Force the end of an open specification item.
+ */
+ protected void cleanUpLastItem()
+ {
+ if (this.inSpecificationItem)
+ {
+ endItem();
+ }
+ }
+
+ /**
+ * Informs the listener about a new specification item, including the ID and
+ * file and line where it was detected.
+ */
+ protected void informListenerAboutNewItem()
+ {
+ final String idText = this.stateMachine.getLastToken();
+ final SpecificationItemId id = new SpecificationItemId.Builder(idText).build();
+ this.listener.beginSpecificationItem();
+ this.listener.setId(id);
+ this.listener.setLocation(this.file.getPath(), this.currentContext.lineNumber());
+ if (this.lastTitle != null)
+ {
+ this.listener.setTitle(this.lastTitle);
+ }
+ }
+
+ /**
+ * End a specification item gracefully.
+ *
+ * As opposed to forcing an end at clean-up (see
+ * {@link LightWeightMarkupImporter#cleanUpLastItem()}.
+ *
+ */
+ protected void endItem()
+ {
+ this.inSpecificationItem = false;
+ resetTitle();
+ this.listener.endSpecificationItem();
+ }
+
+ /**
+ * Set the specification item status.
+ */
+ protected void setStatus()
+ {
+ this.listener.setStatus(ItemStatus.parseString(this.stateMachine.getLastToken()));
+ }
+
+ /**
+ * Begin the textual description of the specification item.
+ */
+ protected void beginDescription()
+ {
+ this.listener.appendDescription(this.stateMachine.getLastToken());
+ }
+
+ /**
+ * Append text to an existing piece of the specification item description.
+ */
+ protected void appendDescription()
+ {
+ this.listener.appendDescription(System.lineSeparator());
+ this.listener.appendDescription(this.stateMachine.getLastToken());
+ }
+
+ /**
+ * Begin the rationale.
+ */
+ protected void beginRationale()
+ {
+ this.listener.appendRationale(System.lineSeparator());
+ }
+
+ /**
+ * Append text to an existing piece of the rationale.
+ */
+ protected void appendRationale()
+ {
+ this.listener.appendRationale(System.lineSeparator());
+ this.listener.appendRationale(this.stateMachine.getLastToken());
+ }
+
+ /**
+ * Begin a comment.
+ */
+ protected void beginComment()
+ {
+ this.listener.appendComment(this.stateMachine.getLastToken());
+ }
+
+ /**
+ * Append text to an existing piece of the comment.
+ */
+ protected void appendComment()
+ {
+ this.listener.appendComment(System.lineSeparator());
+ this.listener.appendComment(this.stateMachine.getLastToken());
+ }
+
+ /**
+ * Add a dependency on another specification item by ID.
+ */
+ protected void addDependency()
+ {
+ final SpecificationItemId.Builder builder = new SpecificationItemId.Builder(
+ this.stateMachine.getLastToken());
+ this.listener.addDependsOnId(builder.build());
+ }
+
+ /**
+ * Add artifact types that this specification item needs to be covered in.
+ */
+ protected void addNeeds()
+ {
+ final String artifactTypes = this.stateMachine.getLastToken();
+ for (final String artifactType : artifactTypes.split(","))
+ {
+ this.listener.addNeededArtifactType(artifactType.trim());
+ }
+ }
+
+ /**
+ * Remember the last section title in case this turns out to be a
+ * specification item.
+ */
+ // [impl->dsn~md.specification-item-title~1]
+ protected void rememberTitle()
+ {
+ this.lastTitle = this.stateMachine.getLastToken();
+ }
+
+ /**
+ * Reset the stored section title.
+ */
+ protected void resetTitle()
+ {
+ this.lastTitle = null;
+ }
+
+ /**
+ * Add an ID for a specification item this one covers.
+ */
+ protected void addCoverage()
+ {
+ this.listener.addCoveredId(SpecificationItemId.parseId(this.stateMachine.getLastToken()));
+ }
+
+ /**
+ * Add one or more tags.
+ */
+ protected void addTag()
+ {
+ final String tags = this.stateMachine.getLastToken();
+ for (final String tag : tags.split(","))
+ {
+ this.listener.addTag(tag.trim());
+ }
+ }
+
+ /**
+ * Create a specification item from a forward marker.
+ */
+ // [impl->dsn~md.artifact-forwarding-notation~1]
+ protected void forward()
+ {
+ final ForwardingSpecificationItem forward = new ForwardingSpecificationItem(
+ this.stateMachine.getLastToken());
+ this.listener.beginSpecificationItem();
+ this.listener.setId(forward.getSkippedId());
+ this.listener.addCoveredId(forward.getOriginalId());
+ for (final String targetArtifactType : forward.getTargetArtifactTypes())
+ {
+ this.listener.addNeededArtifactType(targetArtifactType.trim());
+ }
+ this.listener.setForwards(true);
+ this.listener.setLocation(this.file.getPath(), this.currentContext.lineNumber());
+ this.listener.endSpecificationItem();
+ }
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContext.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContext.java
new file mode 100644
index 000000000..3151259de
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContext.java
@@ -0,0 +1,19 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader;
+
+/**
+ * State of the {@link LineReader} at a given line.
+ *
+ * @param lineNumber
+ * the current line number, starting with 1 for the first line
+ * @param previousLine
+ * the previous line or {@code null} if the current line is the first
+ * line
+ * @param currentLine
+ * the current line, never {@code null}
+ * @param nextLine
+ * the next line or {@code null} if the current line is the last line
+ */
+public record LineContext(int lineNumber, String previousLine, String currentLine, String nextLine)
+{
+
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReader.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReader.java
new file mode 100644
index 000000000..1f1664364
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReader.java
@@ -0,0 +1,74 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import org.itsallcode.openfasttrace.api.importer.ImporterException;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+
+/**
+ * Read a file line by line and call a callback for each line.
+ */
+public class LineReader
+{
+ private static final Logger LOG = Logger.getLogger(LineReader.class.getName());
+
+ private final InputFile file;
+ private final LineReaderCallback callback;
+
+ /**
+ * Create a new {@link LineReader}.
+ *
+ * @param file
+ * the file to read
+ * @param callback
+ * the callback to call for each line
+ */
+ public LineReader(final InputFile file, final LineReaderCallback callback)
+ {
+ this.file = file;
+ this.callback = callback;
+ }
+
+ /**
+ * Start reading the file and call
+ * {@link LineReaderCallback#nextLine(LineContext)} for each line. After
+ * reading the last line, this will call
+ * {@link LineReaderCallback#finishReading()}.
+ */
+ public void readFile()
+ {
+ LOG.finest(() -> "Starting import of file '" + this.file + "'");
+ String previousLine = null;
+ String currentLine = null;
+ String nextLine = null;
+ int lineNumber = 0;
+ try (BufferedReader reader = this.file.createReader())
+ {
+ while ((nextLine = reader.readLine()) != null)
+ {
+ if (currentLine != null)
+ {
+ callback.nextLine(new LineContext(lineNumber, previousLine, currentLine, nextLine));
+ }
+ ++lineNumber;
+ previousLine = currentLine;
+ currentLine = nextLine;
+ }
+ if (currentLine != null)
+ {
+ callback.nextLine(new LineContext(lineNumber, previousLine, currentLine, nextLine));
+ }
+ }
+ catch (final IOException exception)
+ {
+ throw new ImporterException(
+ "Error reading '" + this.file.getPath() + "' at line " + lineNumber + ": "
+ + exception.getMessage(),
+ exception);
+
+ }
+ callback.finishReading();
+ }
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderCallback.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderCallback.java
new file mode 100644
index 000000000..e9697a27a
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderCallback.java
@@ -0,0 +1,21 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader;
+
+/**
+ * Callback for the {@link LineReader} to notify the caller about the lines that
+ * have been read.
+ */
+public interface LineReaderCallback
+{
+ /**
+ * Notify the caller about the next line that has been read.
+ *
+ * @param context
+ * contains the current line and the surrounding lines
+ */
+ void nextLine(LineContext context);
+
+ /**
+ * Notify the caller that the file has been read completely.
+ */
+ void finishReading();
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserState.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserState.java
new file mode 100644
index 000000000..7bd3cc73b
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserState.java
@@ -0,0 +1,34 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+/**
+ * This enum defines the state the line parser for lightweight markup languages
+ * can be in.
+ */
+public enum LineParserState
+{
+ /**
+ * Parser started (at beginning of the file) or outside of a specification
+ * item
+ */
+ START,
+ /** Inside a specification item */
+ SPEC_ITEM,
+ /** Inside a description section */
+ DESCRIPTION,
+ /** Inside a provided coverage section */
+ COVERS,
+ /** Inside a section describing dependencies */
+ DEPENDS,
+ /** Inside a rationale section */
+ RATIONALE,
+ /** Inside a comment section */
+ COMMENT,
+ /** Inside a section defining the required coverage */
+ NEEDS,
+ /** Found a title */
+ TITLE,
+ /** Found tags */
+ TAGS,
+ /** Reached the end of the file */
+ EOF
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachine.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachine.java
new file mode 100644
index 000000000..853437988
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachine.java
@@ -0,0 +1,109 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * This machine implements the core of a state based parser.
+ *
+ * Before the state machine is run, it needs to be configured with a transition
+ * table in the constructor.
+ *
+ *
+ * Each step of the state machine gets a portion of the text to be imported as
+ * input. The machine checks the current state and the input on each step and
+ * decides on resulting state and action depending on the configuration provided
+ * in the transition table.
+ *
+ */
+public class LineParserStateMachine
+{
+ private static final Logger LOG = Logger.getLogger(LineParserStateMachine.class.getName());
+
+ private LineParserState state = LineParserState.START;
+ private String lastToken = "";
+ private final Transition[] transitions;
+
+ /**
+ * Create a new instance of the {@link LineParserStateMachine}
+ *
+ * @param transitions
+ * the transition table that serves as configuration for the
+ * state machine
+ */
+ public LineParserStateMachine(final Transition[] transitions)
+ {
+ this.transitions = Arrays.copyOf(transitions, transitions.length);
+ }
+
+ /**
+ * Step the state machine.
+ *
+ * @param line
+ * the text fragment on which the state machine decides the next
+ * state and action
+ * @param nextLine
+ * the following line or {@code null} if the current line is the
+ * last one in the file. This is useful as a lookahead for
+ * patterns that span multiple lines like underlined titles in
+ * Markdown or RST.
+ */
+ public void step(final String line, final String nextLine)
+ {
+ boolean matched = false;
+ for (final Transition entry : this.transitions)
+ {
+ if ((this.state == entry.getFrom()) && matchToken(line, nextLine, entry))
+ {
+ LOG.finest(() -> entry + " : '" + line + "'");
+ entry.getTransitionAction().transit();
+ this.state = entry.getTo();
+ matched = true;
+ break;
+ }
+ }
+ if (!matched)
+ {
+ LOG.finest(() -> "Current state: " + this.state + ", no match for '" + line + "'");
+ }
+ }
+
+ private boolean matchToken(final String line, final String nextLine, final Transition entry)
+ {
+ final Optional> matches = entry.getLinePattern().getMatches(line, nextLine);
+ if (matches.isPresent())
+ {
+ final List groups = matches.get();
+ this.lastToken = groups.isEmpty() ? "" : groups.get(0);
+ return true;
+ }
+ else
+ {
+ this.lastToken = "";
+ return false;
+ }
+ }
+
+ /**
+ * Get the last text token that the state machine isolated
+ *
+ * @return the last text token
+ */
+ public String getLastToken()
+ {
+ return this.lastToken;
+ }
+
+ /**
+ * Get the current state of the state machine.
+ *
+ * This method is package private because it used only for testing.
+ *
+ *
+ * @return the current state of the state machine
+ */
+ LineParserState getState()
+ {
+ return this.state;
+ }
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LinePattern.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LinePattern.java
new file mode 100644
index 000000000..23968c63e
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LinePattern.java
@@ -0,0 +1,28 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Common interface for text patterns used in line parsers.
+ */
+public interface LinePattern
+{
+ /**
+ * Get the matching groups of the regular expression pattern in the given
+ * line and its following line.
+ *
+ * Implementors are free to ignore the following line if it is not needed.
+ *
+ *
+ * @param line
+ * the current line
+ * @param nextLine
+ * the following line or {@code null} if the current line is the
+ * last line
+ * @return list of matching groups or an empty optional if the pattern does
+ * not match. If the pattern does not have any groups, the list will
+ * be empty.
+ */
+ Optional> getMatches(final String line, final String nextLine);
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePattern.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePattern.java
new file mode 100644
index 000000000..d0cbfac6f
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePattern.java
@@ -0,0 +1,48 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Simple {@link LinePattern} implementation that only considers the current
+ * line and not the following line.
+ */
+public final class SimpleLinePattern implements LinePattern
+{
+ private final Pattern pattern;
+
+ private SimpleLinePattern(final Pattern pattern)
+ {
+ this.pattern = pattern;
+ }
+
+ /**
+ * Create a new instance of a {@link SimpleLinePattern}.
+ *
+ * @param pattern
+ * the regular expression pattern to match, potentially
+ * containing groups, see {@link Pattern#compile(String)}
+ * @return a new instance of a {@link SimpleLinePattern}
+ */
+ public static SimpleLinePattern of(final String pattern)
+ {
+ return new SimpleLinePattern(Pattern.compile(pattern));
+ }
+
+ @Override
+ public Optional> getMatches(final String line, final String nextLine)
+ {
+ final Matcher matcher = pattern.matcher(line);
+ if (matcher.matches())
+ {
+ final List matches = new ArrayList<>();
+ for (int i = 1; i <= matcher.groupCount(); i++)
+ {
+ matches.add(matcher.group(i));
+ }
+ return Optional.of(matches);
+ }
+ return Optional.empty();
+ }
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/Transition.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/Transition.java
new file mode 100644
index 000000000..8d0f232f0
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/Transition.java
@@ -0,0 +1,81 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+/**
+ * Transition in the line parser statemachine.
+ */
+public class Transition
+{
+ private final LineParserState from;
+ private final LineParserState to;
+ private final LinePattern linePattern;
+ private final TransitionAction transitionAction;
+
+ /**
+ * Create a new instance of a {@link Transition}.
+ *
+ * @param from
+ * state the statemachine comes from
+ * @param to
+ * state the machine will switch to if the pattern matches
+ * @param linePattern
+ * pattern the line must match for the transition to happen
+ * @param transitionAction
+ * action that will be executed as result of the transition
+ */
+ public Transition(final LineParserState from, final LineParserState to, final LinePattern linePattern,
+ final TransitionAction transitionAction)
+ {
+ this.from = from;
+ this.to = to;
+ this.linePattern = linePattern;
+ this.transitionAction = transitionAction;
+ }
+
+ /**
+ * Get the origin state of this transition.
+ *
+ * @return origin state
+ */
+ public LineParserState getFrom()
+ {
+ return this.from;
+ }
+
+ /**
+ * Get the target state of this transition.
+ *
+ * @return target state
+ */
+ public LineParserState getTo()
+ {
+ return this.to;
+ }
+
+ /**
+ * Get the regular expression pattern that needs to be matched in order for
+ * the transition to happen.
+ *
+ * @return line pattern to be matched
+ */
+ public LinePattern getLinePattern()
+ {
+ return this.linePattern;
+ }
+
+ /**
+ * Get the action that is executed when the transition happens.
+ *
+ * @return action that is executed as result of the transition
+ */
+ public TransitionAction getTransitionAction()
+ {
+ return this.transitionAction;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Transition [from=" + this.from + ", to=" + this.to + ", markdownPattern="
+ + this.linePattern + "]";
+ }
+}
diff --git a/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/TransitionAction.java b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/TransitionAction.java
new file mode 100644
index 000000000..58d90329a
--- /dev/null
+++ b/importer/lightweightmarkup/src/main/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/TransitionAction.java
@@ -0,0 +1,13 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+/**
+ * Action that is executed as a result of a state transition in the line parser.
+ */
+@FunctionalInterface
+public interface TransitionAction
+{
+ /**
+ * Execute the transition action.
+ */
+ void transit();
+}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownForwardingSpecificationItemTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/ForwardingSpecificationItemTest.java
similarity index 78%
rename from importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownForwardingSpecificationItemTest.java
rename to importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/ForwardingSpecificationItemTest.java
index e10084d25..4cb44ee4b 100644
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownForwardingSpecificationItemTest.java
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/ForwardingSpecificationItemTest.java
@@ -1,19 +1,19 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
-import org.junit.jupiter.api.Test;
+package org.itsallcode.openfasttrace.importer.lightweightmarkup;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.junit.jupiter.api.Test;
-class MarkdownForwardingSpecificationItemTest
+class ForwardingSpecificationItemTest
{
@Test
void parseForwardInstrcution()
{
- final MarkdownForwardingSpecificationItem item = new MarkdownForwardingSpecificationItem(
+ final ForwardingSpecificationItem item = new ForwardingSpecificationItem(
"arch --> dsn : req~web-ui-uses-corporate-design~1");
assertAll(
() -> assertThat(item.getSkippedArtifactType(), equalTo("arch")),
@@ -23,4 +23,4 @@ void parseForwardInstrcution()
() -> assertThat(item.getOriginalId(),
equalTo(SpecificationItemId.parseId("req~web-ui-uses-corporate-design~1"))));
}
-}
\ No newline at end of file
+}
diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/TransitionTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/TransitionTest.java
new file mode 100644
index 000000000..42e8c4753
--- /dev/null
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/TransitionTest.java
@@ -0,0 +1,25 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.when;
+
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class TransitionTest
+{
+ @Test
+ void testToString(@Mock final TransitionAction actionMock, @Mock final LinePattern patternMock)
+ {
+ when(patternMock.toString()).thenReturn("DUMMY_PATTERN");
+ final Transition transition = new Transition(LineParserState.COMMENT, LineParserState.TITLE, patternMock,
+ actionMock);
+ assertThat(transition.toString(),
+ equalTo("Transition [from=COMMENT, to=TITLE, markdownPattern=DUMMY_PATTERN]"));
+ }
+}
diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContextTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContextTest.java
new file mode 100644
index 000000000..d58395f9f
--- /dev/null
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineContextTest.java
@@ -0,0 +1,22 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader;
+
+import org.junit.jupiter.api.Test;
+
+import com.jparams.verifier.tostring.ToStringVerifier;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+class LineContextTest
+{
+ @Test
+ void testEqualsAndHashContract()
+ {
+ EqualsVerifier.forClass(LineContext.class).verify();
+ }
+
+ @Test
+ void testToString()
+ {
+ ToStringVerifier.forClass(LineContext.class).verify();
+ }
+}
diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderTest.java
new file mode 100644
index 000000000..75f08cf02
--- /dev/null
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/linereader/LineReaderTest.java
@@ -0,0 +1,117 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.linereader;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+import java.io.*;
+
+import org.itsallcode.openfasttrace.api.importer.ImporterException;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class LineReaderTest
+{
+ private static final String FILE_PATH = "file/path";
+
+ @Mock
+ LineReaderCallback callbackMock;
+ InOrder inOrder;
+
+ @BeforeEach
+ void setUp()
+ {
+ inOrder = inOrder(callbackMock);
+ }
+
+ @AfterEach
+ void verifyNoMoreInteractions()
+ {
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "", "\n", "\r\n", "\n\r" })
+ void testReadEmptyFile(final String content) throws IOException
+ {
+ parse(content);
+ inOrder.verify(callbackMock).finishReading();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "", "\n", "\r\n", })
+ void testReadSingleLine(final String lineEnding) throws IOException
+ {
+ parse("line1" + lineEnding);
+ inOrder.verify(callbackMock).nextLine(new LineContext(1, null, "line1", null));
+ inOrder.verify(callbackMock).finishReading();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "\n", "\r\n" })
+ void testReadTwoLines(final String lineEnding) throws IOException
+ {
+ parse("line1" + lineEnding + "line2" + lineEnding);
+ inOrder.verify(callbackMock).nextLine(new LineContext(1, null, "line1", "line2"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(2, "line1", "line2", null));
+ inOrder.verify(callbackMock).finishReading();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "\n", "\r\n" })
+ void testReadThreeLines(final String lineEnding) throws IOException
+ {
+ parse("line1" + lineEnding + "line2" + lineEnding + "line3" + lineEnding);
+ inOrder.verify(callbackMock).nextLine(new LineContext(1, null, "line1", "line2"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(2, "line1", "line2", "line3"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(3, "line2", "line3", null));
+ inOrder.verify(callbackMock).finishReading();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "\n", "\r\n" })
+ void testReadFourLines(final String lineEnding) throws IOException
+ {
+ parse("line1" + lineEnding + "line2" + lineEnding + "line3" + lineEnding + "line4");
+ inOrder.verify(callbackMock).nextLine(new LineContext(1, null, "line1", "line2"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(2, "line1", "line2", "line3"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(3, "line2", "line3", "line4"));
+ inOrder.verify(callbackMock).nextLine(new LineContext(4, "line3", "line4", null));
+ inOrder.verify(callbackMock).finishReading();
+ }
+
+ @Test
+ void testReadFails(@Mock final BufferedReader readerMock) throws IOException
+ {
+ when(readerMock.readLine()).thenThrow(new IOException("mock"));
+ final ImporterException exception = assertThrows(ImporterException.class, () -> parse(readerMock));
+ assertThat(exception.getMessage(), equalTo("Error reading '" + FILE_PATH + "' at line 0: mock"));
+ }
+
+ private void parse(final String content) throws IOException
+ {
+ final BufferedReader reader = new BufferedReader(new StringReader(content));
+ parse(reader);
+ }
+
+ private void parse(final BufferedReader reader) throws IOException
+ {
+ final InputFile inputFileMock = mock(InputFile.class);
+ lenient().when(inputFileMock.getPath()).thenReturn(FILE_PATH);
+ when(inputFileMock.createReader()).thenReturn(reader);
+ new LineReader(inputFileMock, callbackMock).readFile();
+ }
+}
diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachineTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachineTest.java
new file mode 100644
index 000000000..1d31ca025
--- /dev/null
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/LineParserStateMachineTest.java
@@ -0,0 +1,121 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class LineParserStateMachineTest
+{
+ LineParserStateMachine stateMachine;
+ @Mock
+ TransitionAction actionMock;
+
+ @Test
+ void testNoTransitions()
+ {
+ setupTransitions();
+ step("line");
+ assertTransition(LineParserState.START, "");
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ private void setupTransitions(final Transition... transitions)
+ {
+ this.stateMachine = new LineParserStateMachine(transitions);
+ }
+
+ private void assertTransition(final LineParserState expectedState, final String expectedToken)
+ {
+ assertAll(() -> assertThat("next state", this.stateMachine.getState(), equalTo(expectedState)),
+ () -> assertThat("token", this.stateMachine.getLastToken(), equalTo(expectedToken)));
+ }
+
+ private void step(final String inputLine)
+ {
+ this.step(inputLine, null);
+ }
+
+ private void step(final String inputLine, final String nextInputLine)
+ {
+ this.stateMachine.step(inputLine, nextInputLine);
+ }
+
+ @Test
+ void testMatchedSingleTransitionWithMock(@Mock final LinePattern patternMock)
+ {
+ when(patternMock.getMatches("line1", "line2")).thenReturn(Optional.of(List.of("result", "ignored")));
+ setupTransitions(transition(LineParserState.START, LineParserState.COMMENT, patternMock));
+ step("line1", "line2");
+ assertTransition(LineParserState.COMMENT, "result");
+ verify(actionMock).transit();
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ @Test
+ void testNotMatchedTransitionWithMock(@Mock final LinePattern patternMock)
+ {
+ when(patternMock.getMatches("line1", "line2")).thenReturn(Optional.empty());
+ setupTransitions(transition(LineParserState.START, LineParserState.COMMENT, patternMock));
+ step("line1", "line2");
+ assertTransition(LineParserState.START, "");
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ @Test
+ void testMatchedTransitionInNextLine()
+ {
+ setupTransitions(transition(LineParserState.START, LineParserState.COMMENT, pattern("(line)")));
+ step("line");
+ assertTransition(LineParserState.COMMENT, "line");
+ verify(actionMock).transit();
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ @Test
+ void testMatchedTransitionNoToken()
+ {
+ setupTransitions(transition(LineParserState.START, LineParserState.COMMENT, pattern("line")));
+ step("line");
+ assertTransition(LineParserState.COMMENT, "");
+ verify(actionMock).transit();
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ @Test
+ void testWrongState()
+ {
+ setupTransitions(transition(LineParserState.COMMENT, LineParserState.COMMENT, pattern("line")));
+ step("line");
+ assertTransition(LineParserState.START, "");
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ @Test
+ void testNoMatch()
+ {
+ setupTransitions(transition(LineParserState.START, LineParserState.COMMENT, pattern("notmatching")));
+ step("line");
+ assertTransition(LineParserState.START, "");
+ verifyNoMoreInteractions(actionMock);
+ }
+
+ private LinePattern pattern(final String pattern)
+ {
+ return SimpleLinePattern.of(pattern);
+ }
+
+ private Transition transition(final LineParserState from, final LineParserState to, final LinePattern pattern)
+ {
+ return new Transition(from, to, pattern, actionMock);
+ }
+}
diff --git a/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePatternTest.java b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePatternTest.java
new file mode 100644
index 000000000..29ee249ed
--- /dev/null
+++ b/importer/lightweightmarkup/src/test/java/org/itsallcode/openfasttrace/importer/lightweightmarkup/statemachine/SimpleLinePatternTest.java
@@ -0,0 +1,53 @@
+package org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.itsallcode.matcher.auto.AutoMatcher.equalTo;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class SimpleLinePatternTest
+{
+ static Stream testCases()
+ {
+ return Stream.of(
+ testCase("abc", "abc", List.of()),
+ testCase("abc", "def", null),
+ testCase("([0-9]+)", "123", List.of("123")),
+ testCase("([0-9]+)(\\w+)", "123abc", List.of("123", "abc")),
+ testCase("(?:[0-9]+)", "123", List.of()),
+ testCase("[0-9]+", "123", List.of()),
+ testCase("([0-9]+)", "abc", null));
+ }
+
+ private static Arguments testCase(final String pattern, final String line, final List expected)
+ {
+ return Arguments.of(pattern, line, expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("testCases")
+ void test(final String pattern, final String line, final List expected)
+ {
+ final Optional> matches = SimpleLinePattern.of(pattern).getMatches(line, null);
+ if (expected == null)
+ {
+ assertThat("Text '" + line + "' should not match pattern '" + pattern + "'", matches.isPresent(),
+ is(false));
+ }
+ else
+ {
+ assertAll(
+ () -> assertThat("Text '" + line + "' should match pattern '" + pattern + "'", matches.isPresent(),
+ is(true)),
+ () -> assertThat(matches.get(), equalTo(expected)));
+ }
+ }
+}
diff --git a/importer/markdown/.settings/org.eclipse.jdt.core.prefs b/importer/markdown/.settings/org.eclipse.jdt.core.prefs
index 04b36440c..a40522564 100644
--- a/importer/markdown/.settings/org.eclipse.jdt.core.prefs
+++ b/importer/markdown/.settings/org.eclipse.jdt.core.prefs
@@ -1,6 +1,6 @@
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
@@ -20,7 +20,7 @@ org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/importer/markdown/pom.xml b/importer/markdown/pom.xml
index 4e6b034a0..afc2f622f 100644
--- a/importer/markdown/pom.xml
+++ b/importer/markdown/pom.xml
@@ -18,10 +18,14 @@
org.itsallcode.openfasttrace
openfasttrace-api
+
+ org.itsallcode.openfasttrace
+ openfasttrace-importer-lightweightmarkup
+
org.itsallcode.openfasttrace
openfasttrace-testutil
test
-
\ No newline at end of file
+
diff --git a/importer/markdown/src/main/java/module-info.java b/importer/markdown/src/main/java/module-info.java
index a4c289ddc..e6e6430e8 100644
--- a/importer/markdown/src/main/java/module-info.java
+++ b/importer/markdown/src/main/java/module-info.java
@@ -1,13 +1,15 @@
+import org.itsallcode.openfasttrace.importer.markdown.MarkdownImporterFactory;
+
/**
- * This provides an importer for the MarkDown format.
+ * This provides an importer for the Markdown format.
*
* @provides org.itsallcode.openfasttrace.api.importer.ImporterFactory
*/
module org.itsallcode.openfasttrace.importer.markdown
{
- requires java.logging;
requires transitive org.itsallcode.openfasttrace.api;
+ requires org.itsallcode.openfasttrace.importer.lightweightmarkup;
provides org.itsallcode.openfasttrace.api.importer.ImporterFactory
- with org.itsallcode.openfasttrace.importer.markdown.MarkdownImporterFactory;
+ with MarkdownImporterFactory;
}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java
index 63a67d941..4014375bb 100644
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java
+++ b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporter.java
@@ -1,329 +1,169 @@
package org.itsallcode.openfasttrace.importer.markdown;
-import static org.itsallcode.openfasttrace.importer.markdown.State.*;
+import static org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LineParserState.*;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.util.logging.Logger;
-
-import org.itsallcode.openfasttrace.api.core.ItemStatus;
-import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
-import org.itsallcode.openfasttrace.api.importer.*;
+import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
-
-class MarkdownImporter implements Importer
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.LightWeightMarkupImporter;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*;
+
+/**
+ * Importer for OFT augmented Markdown.
+ *
+ * The purpose of this importer is to find specification items that follow a
+ * certain structure inside Markdown documents. It is not the goal to ingest the
+ * complete markdown document though, only the specification items. Also, the
+ * hierarchical structure of the document itself has no impact on the
+ * specification items. OFT just extracts a flat list. Linking the items is
+ * explicitly not the purpose of the importer.
+ *
+ */
+class MarkdownImporter extends LightWeightMarkupImporter
{
- private static final Logger LOG = Logger.getLogger(MarkdownImporter.class.getName());
-
- // @formatter:off
- private final Transition[] transitions = {
- transition(START , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(START , TITLE , MdPattern.TITLE , this::rememberTitle ),
- transition(START , OUTSIDE , MdPattern.FORWARD , this::forward ),
- transition(START , OUTSIDE , MdPattern.EVERYTHING , () -> {} ),
-
- transition(TITLE , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(TITLE , TITLE , MdPattern.TITLE , this::rememberTitle ),
- transition(TITLE , TITLE , MdPattern.EMPTY , () -> {} ),
- transition(TITLE , OUTSIDE , MdPattern.EVERYTHING , this::resetTitle ),
-
- transition(OUTSIDE , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(OUTSIDE , OUTSIDE , MdPattern.FORWARD , this::forward ),
- transition(OUTSIDE , TITLE , MdPattern.TITLE , this::rememberTitle ),
- transition(OUTSIDE , TITLE , MdPattern.UNDERLINE , this::rememberPreviousLineAsTitle ),
-
- transition(SPEC_ITEM , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(SPEC_ITEM , SPEC_ITEM , MdPattern.STATUS , this::setStatus ),
- transition(SPEC_ITEM , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(SPEC_ITEM , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(SPEC_ITEM , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(SPEC_ITEM , COVERS , MdPattern.COVERS , () -> {} ),
- transition(SPEC_ITEM , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(SPEC_ITEM , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(SPEC_ITEM , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(SPEC_ITEM , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(SPEC_ITEM , TAGS , MdPattern.TAGS , () -> {} ),
- transition(SPEC_ITEM , DESCRIPTION, MdPattern.DESCRIPTION, this::beginDescription ),
- transition(SPEC_ITEM , DESCRIPTION, MdPattern.NOT_EMPTY , this::beginDescription ),
-
- transition(DESCRIPTION, SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(DESCRIPTION, TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(DESCRIPTION, RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(DESCRIPTION, COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(DESCRIPTION, COVERS , MdPattern.COVERS , () -> {} ),
- transition(DESCRIPTION, DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(DESCRIPTION, NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(DESCRIPTION, NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(DESCRIPTION, TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(DESCRIPTION, TAGS , MdPattern.TAGS , () -> {} ),
- transition(DESCRIPTION, DESCRIPTION, MdPattern.EVERYTHING , this::appendDescription ),
-
-
- transition(RATIONALE , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(RATIONALE , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(RATIONALE , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(RATIONALE , COVERS , MdPattern.COVERS , () -> {} ),
- transition(RATIONALE , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(RATIONALE , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(RATIONALE , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(RATIONALE , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(RATIONALE , TAGS , MdPattern.TAGS , () -> {} ),
- transition(RATIONALE , RATIONALE , MdPattern.EVERYTHING , this::appendRationale ),
-
- transition(COMMENT , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(COMMENT , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(COMMENT , COVERS , MdPattern.COVERS , () -> {} ),
- transition(COMMENT , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(COMMENT , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(COMMENT , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(COMMENT , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(COMMENT , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(COMMENT , TAGS , MdPattern.TAGS , () -> {} ),
- transition(COMMENT , COMMENT , MdPattern.EVERYTHING , this::appendComment ),
-
-
- // [impl->dsn~md.covers-list~1]
- transition(COVERS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(COVERS , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(COVERS , COVERS , MdPattern.COVERS_REF , this::addCoverage ),
- transition(COVERS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(COVERS , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(COVERS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(COVERS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(COVERS , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(COVERS , COVERS , MdPattern.EMPTY , () -> {} ),
- transition(COVERS , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(COVERS , TAGS , MdPattern.TAGS , () -> {} ),
-
- // [impl->dsn~md.depends-list~1]
- transition(DEPENDS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(DEPENDS , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(DEPENDS , DEPENDS , MdPattern.DEPENDS_REF, this::addDependency ),
- transition(DEPENDS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(DEPENDS , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(DEPENDS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(DEPENDS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(DEPENDS , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(DEPENDS , DEPENDS , MdPattern.EMPTY , () -> {} ),
- transition(DEPENDS , COVERS , MdPattern.COVERS , () -> {} ),
- transition(DEPENDS , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(DEPENDS , TAGS , MdPattern.TAGS , () -> {} ),
-
- // [impl->dsn~md.needs-coverage-list~2]
- // [impl->dsn~md.needs-coverage-list-compact~1]
- transition(NEEDS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(NEEDS , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(NEEDS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(NEEDS , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(NEEDS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(NEEDS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(NEEDS , NEEDS , MdPattern.NEEDS_REF , this::addNeeds ),
- transition(NEEDS , NEEDS , MdPattern.EMPTY , () -> {} ),
- transition(NEEDS , COVERS , MdPattern.COVERS , () -> {} ),
- transition(NEEDS , TAGS , MdPattern.TAGS_INT , this::addTag ),
- transition(NEEDS , TAGS , MdPattern.TAGS , () -> {} ),
-
- transition(TAGS , TAGS , MdPattern.TAG_ENTRY , this::addTag ),
- transition(TAGS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
- transition(TAGS , TITLE , MdPattern.TITLE , () -> {endItem(); rememberTitle();} ),
- transition(TAGS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
- transition(TAGS , COMMENT , MdPattern.COMMENT , this::beginComment ),
- transition(TAGS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
- transition(TAGS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
- transition(TAGS , NEEDS , MdPattern.NEEDS , () -> {} ),
- transition(TAGS , NEEDS , MdPattern.EMPTY , () -> {} ),
- transition(TAGS , COVERS , MdPattern.COVERS , () -> {} ),
- transition(TAGS , TAGS , MdPattern.TAGS , () -> {} ),
- transition(TAGS , TAGS , MdPattern.TAGS_INT , this::addTag )
- };
- // @formatter:on
-
- private final InputFile file;
- private final ImportEventListener listener;
- private final MarkdownImporterStateMachine stateMachine;
- private String lastTitle = null;
- private String lastLine = null;
- private boolean inSpecificationItem;
- private int lineNumber = 0;
-
+ private static final LinePattern SECTION_TITLE = new MdSectionTitlePattern();
+
+ /**
+ * Creates a {@link MarkdownImporter} object with the given parameters.
+ *
+ * @param fileName
+ * the input file to be imported
+ * @param listener
+ * the listener to handle import events
+ */
MarkdownImporter(final InputFile fileName, final ImportEventListener listener)
{
- this.file = fileName;
- this.listener = listener;
- this.stateMachine = new MarkdownImporterStateMachine(this.transitions);
- }
-
- @Override
- public void runImport()
- {
- LOG.fine(() -> "Starting import of file " + this.file);
- String line;
- this.lineNumber = 0;
- try (BufferedReader reader = this.file.createReader())
- {
- while ((line = reader.readLine()) != null)
- {
- ++this.lineNumber;
- this.stateMachine.step(line);
- this.lastLine = line;
- }
- }
- catch (final IOException exception)
- {
- throw new ImporterException(
- "Error reading \"" + this.file.getPath() + "\" at line " + this.lineNumber,
- exception);
-
- }
- finishImport();
- }
-
- private void finishImport()
- {
- if (this.inSpecificationItem)
- {
- this.listener.endSpecificationItem();
- }
- }
-
- private static Transition transition(final State from, final State to,
+ super(fileName, listener);
+ }
+
+ protected Transition[] configureTransitions()
+ {
+ // @formatter:off
+ return new Transition[]{
+ transition(START , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(START , TITLE , SECTION_TITLE , this::rememberTitle ),
+ transition(START , START , MdPattern.FORWARD , this::forward ),
+ transition(START , START , MdPattern.EVERYTHING , () -> {} ),
+
+ transition(TITLE , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(TITLE , TITLE , SECTION_TITLE , this::rememberTitle ),
+ transition(TITLE , TITLE , MdPattern.UNDERLINE , () -> {} ),
+ transition(TITLE , TITLE , MdPattern.EMPTY , () -> {} ),
+ transition(TITLE , START , MdPattern.FORWARD , () -> {forward(); resetTitle();} ),
+ transition(TITLE , START , MdPattern.EVERYTHING , this::resetTitle ),
+
+ transition(SPEC_ITEM , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(SPEC_ITEM , SPEC_ITEM , MdPattern.STATUS , this::setStatus ),
+ transition(SPEC_ITEM , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(SPEC_ITEM , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(SPEC_ITEM , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(SPEC_ITEM , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(SPEC_ITEM , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(SPEC_ITEM , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(SPEC_ITEM , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(SPEC_ITEM , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(SPEC_ITEM , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(SPEC_ITEM , DESCRIPTION, MdPattern.DESCRIPTION, this::beginDescription ),
+ transition(SPEC_ITEM , DESCRIPTION, MdPattern.NOT_EMPTY , this::beginDescription ),
+
+ transition(DESCRIPTION, SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(DESCRIPTION, TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(DESCRIPTION, RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(DESCRIPTION, COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(DESCRIPTION, COVERS , MdPattern.COVERS , () -> {} ),
+ transition(DESCRIPTION, DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(DESCRIPTION, NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(DESCRIPTION, NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(DESCRIPTION, TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(DESCRIPTION, TAGS , MdPattern.TAGS , () -> {} ),
+ transition(DESCRIPTION, DESCRIPTION, MdPattern.EVERYTHING , this::appendDescription ),
+
+ transition(RATIONALE , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(RATIONALE , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(RATIONALE , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(RATIONALE , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(RATIONALE , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(RATIONALE , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(RATIONALE , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(RATIONALE , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(RATIONALE , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(RATIONALE , RATIONALE , MdPattern.EVERYTHING , this::appendRationale ),
+
+ transition(COMMENT , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(COMMENT , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(COMMENT , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(COMMENT , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(COMMENT , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(COMMENT , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(COMMENT , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(COMMENT , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(COMMENT , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(COMMENT , COMMENT , MdPattern.EVERYTHING , this::appendComment ),
+
+
+ // [impl->dsn~md.covers-list~1]
+ transition(COVERS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(COVERS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(COVERS , COVERS , MdPattern.COVERS_REF , this::addCoverage ),
+ transition(COVERS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(COVERS , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(COVERS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(COVERS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(COVERS , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(COVERS , COVERS , MdPattern.EMPTY , () -> {} ),
+ transition(COVERS , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(COVERS , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(COVERS , START , MdPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ // [impl->dsn~md.depends-list~1]
+ transition(DEPENDS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(DEPENDS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(DEPENDS , DEPENDS , MdPattern.DEPENDS_REF, this::addDependency ),
+ transition(DEPENDS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(DEPENDS , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(DEPENDS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(DEPENDS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(DEPENDS , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(DEPENDS , DEPENDS , MdPattern.EMPTY , () -> {} ),
+ transition(DEPENDS , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(DEPENDS , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(DEPENDS , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(DEPENDS , START , MdPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ // [impl->dsn~md.needs-coverage-list-single-line~2]
+ // [impl->dsn~md.needs-coverage-list~1]
+ transition(NEEDS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(NEEDS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(NEEDS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(NEEDS , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(NEEDS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(NEEDS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(NEEDS , NEEDS , MdPattern.NEEDS_REF , this::addNeeds ),
+ transition(NEEDS , NEEDS , MdPattern.EMPTY , () -> {} ),
+ transition(NEEDS , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(NEEDS , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(NEEDS , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(NEEDS , START , MdPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ transition(TAGS , TAGS , MdPattern.TAG_ENTRY , this::addTag ),
+ transition(TAGS , SPEC_ITEM , MdPattern.ID , this::beginItem ),
+ transition(TAGS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(TAGS , RATIONALE , MdPattern.RATIONALE , this::beginRationale ),
+ transition(TAGS , COMMENT , MdPattern.COMMENT , this::beginComment ),
+ transition(TAGS , DEPENDS , MdPattern.DEPENDS , () -> {} ),
+ transition(TAGS , NEEDS , MdPattern.NEEDS_INT , this::addNeeds ),
+ transition(TAGS , NEEDS , MdPattern.NEEDS , () -> {} ),
+ transition(TAGS , NEEDS , MdPattern.EMPTY , () -> {} ),
+ transition(TAGS , COVERS , MdPattern.COVERS , () -> {} ),
+ transition(TAGS , TAGS , MdPattern.TAGS , () -> {} ),
+ transition(TAGS , TAGS , MdPattern.TAGS_INT , this::addTag ),
+ transition(TAGS , START , MdPattern.FORWARD , () -> {endItem(); forward();} )
+ };
+ // @formatter:on
+ }
+
+ private static Transition transition(final LineParserState from, final LineParserState to,
final MdPattern pattern, final TransitionAction action)
{
- return new Transition(from, to, pattern, action);
- }
-
- private void beginItem()
- {
- cleanUpLastItem();
- this.inSpecificationItem = true;
- informListenerAboutNewItem();
- }
-
- private void cleanUpLastItem()
- {
- if (this.inSpecificationItem)
- {
- endItem();
- }
- }
-
- private void informListenerAboutNewItem()
- {
- final String idText = this.stateMachine.getLastToken();
- final SpecificationItemId id = new SpecificationItemId.Builder(idText).build();
- this.listener.beginSpecificationItem();
- this.listener.setId(id);
- this.listener.setLocation(this.file.getPath(), this.lineNumber);
- if (this.lastTitle != null)
- {
- this.listener.setTitle(this.lastTitle);
- }
- }
-
- private void endItem()
- {
- this.inSpecificationItem = false;
- resetTitle();
- this.listener.endSpecificationItem();
- }
-
- private void setStatus()
- {
- this.listener.setStatus(ItemStatus.parseString(this.stateMachine.getLastToken()));
- }
-
- private void beginDescription()
- {
- this.listener.appendDescription(this.stateMachine.getLastToken());
- }
-
- private void appendDescription()
- {
- this.listener.appendDescription(System.lineSeparator());
- this.listener.appendDescription(this.stateMachine.getLastToken());
- }
-
- private void beginRationale()
- {
- this.listener.appendRationale(System.lineSeparator());
- }
-
- private void appendRationale()
- {
- this.listener.appendRationale(System.lineSeparator());
- this.listener.appendRationale(this.stateMachine.getLastToken());
- }
-
- private void beginComment()
- {
- this.listener.appendComment(this.stateMachine.getLastToken());
- }
-
- private void appendComment()
- {
- this.listener.appendComment(System.lineSeparator());
- this.listener.appendComment(this.stateMachine.getLastToken());
- }
-
- private void addDependency()
- {
- final SpecificationItemId.Builder builder = new SpecificationItemId.Builder(
- this.stateMachine.getLastToken());
- this.listener.addDependsOnId(builder.build());
- }
-
- private void addNeeds()
- {
- final String artifactTypes = this.stateMachine.getLastToken();
- for (final String artifactType : artifactTypes.split(","))
- {
- this.listener.addNeededArtifactType(artifactType.trim());
- }
- }
-
- private void rememberPreviousLineAsTitle() {
- this.lastTitle = this.lastLine;
- }
-
- // [impl->dsn~md.specification-item-title~1]
- private void rememberTitle()
- {
- this.lastTitle = this.stateMachine.getLastToken();
- }
-
-
- private void resetTitle()
- {
- this.lastTitle = null;
- }
-
- private void addCoverage()
- {
- this.listener.addCoveredId(SpecificationItemId.parseId(this.stateMachine.getLastToken()));
- }
-
- private void addTag()
- {
- final String tags = this.stateMachine.getLastToken();
- for (final String tag : tags.split(","))
- {
- this.listener.addTag(tag.trim());
- }
- }
-
- // [impl->dsn~md.artifact-forwarding-notation~1]
- private void forward()
- {
- final MarkdownForwardingSpecificationItem forward = new MarkdownForwardingSpecificationItem(
- this.stateMachine.getLastToken());
- this.listener.beginSpecificationItem();
- this.listener.setId(forward.getSkippedId());
- this.listener.addCoveredId(forward.getOriginalId());
- for (final String targetArtifactType : forward.getTargetArtifactTypes())
- {
- this.listener.addNeededArtifactType(targetArtifactType.trim());
- }
- this.listener.setForwards(true);
- this.listener.endSpecificationItem();
+ return new Transition(from, to, pattern.getPattern(), action);
}
}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporterStateMachine.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporterStateMachine.java
deleted file mode 100644
index 6ae40c30e..000000000
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownImporterStateMachine.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-import java.util.logging.Logger;
-
-import java.util.regex.Matcher;
-
-/**
- * This machine implements the core of a state based parser
- *
- * Before the state machine is run, it needs to be configured with a transition
- * table in the constructor.
- *
- * Each step of the state machine gets a portion of the text to be imported as
- * input. The machine checks the current state and the input on each step and
- * decides on resulting state and action depending on the configuration provided
- * in the transition table.
- */
-public class MarkdownImporterStateMachine
-{
- private static final Logger LOG = Logger
- .getLogger(MarkdownImporterStateMachine.class.getName());
-
- private State state = State.START;
- private String lastToken = "";
- private final Transition[] transitions;
-
- /**
- * Create a new instance of the {@link MarkdownImporterStateMachine}
- *
- * @param transitions
- * the transition table that serves as configuration for the
- * state machine
- */
- public MarkdownImporterStateMachine(final Transition[] transitions)
- {
- this.transitions = transitions;
- }
-
- /**
- * Step the state machine
- *
- * @param line
- * the text fragment on which the state machine decides the next
- * state and action
- */
- public void step(final String line)
- {
- for (final Transition entry : this.transitions)
- {
- if ((this.state == entry.getFrom()) && matchToken(line, entry))
- {
- LOG.finest(() -> entry + " : '" + line + "'");
- entry.getTransition().transit();
- this.state = entry.getTo();
- break;
- }
- }
- }
-
- private boolean matchToken(final String line, final Transition entry)
- {
- boolean matches = false;
- final Matcher matcher = entry.getMarkdownPattern().getPattern().matcher(line);
- if (matcher.matches())
- {
- this.lastToken = (matcher.groupCount() == 0) ? "" : matcher.group(1);
- matches = true;
- }
- return matches;
- }
-
- /**
- * Get the last text token that the state machine isolated
- *
- * @return the last text token
- */
- public String getLastToken()
- {
- return this.lastToken;
- }
-}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java
index f63aa8e19..b5cd0c4ce 100644
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java
+++ b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdPattern.java
@@ -1,8 +1,9 @@
package org.itsallcode.openfasttrace.importer.markdown;
-import java.util.regex.Pattern;
-
import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.ForwardingSpecificationItem;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LinePattern;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.SimpleLinePattern;
/**
* Patterns that describe tokens to be recognized within Markdown-style
@@ -25,18 +26,18 @@ enum MdPattern
FORWARD(".*?("
+ PatternConstants.ARTIFACT_TYPE
+ "\\s*"
- + MarkdownForwardingSpecificationItem.FORWARD_MARKER
+ + ForwardingSpecificationItem.FORWARD_MARKER
+ "\\s*"
+ PatternConstants.ARTIFACT_TYPE
+ "(?:,\\s*"
+ PatternConstants.ARTIFACT_TYPE
+ ")*"
+ "\\s*"
- + MarkdownForwardingSpecificationItem.ORIGINAL_MARKER
+ + ForwardingSpecificationItem.ORIGINAL_MARKER
+ "\\s*"
+ SpecificationItemId.ID_PATTERN
+ ").*?"),
- ID("`?((?:" + SpecificationItemId.ID_PATTERN + ")|(?:" + SpecificationItemId.LEGACY_ID_PATTERN + "))`?.*"),
+ ID("`?(" + SpecificationItemId.ID_PATTERN + ")`?.*"),
NEEDS_INT("Needs:(\\s*\\w+\\s*(?:,\\s*\\w+\\s*)*)"),
NEEDS("Needs:\\s*"),
NEEDS_REF(PatternConstants.UP_TO_3_WHITESPACES + PatternConstants.BULLETS
@@ -52,14 +53,14 @@ enum MdPattern
+ "\\s*" //
+ "(.*)"),
TITLE("#+\\s*(.*)"),
- UNDERLINE("([=-]{3,})");
+ UNDERLINE("([=-]{3,})\\s*");
// @formatter:on
- private final Pattern pattern;
+ private final LinePattern pattern;
MdPattern(final String regularExpression)
{
- this.pattern = Pattern.compile(regularExpression);
+ this.pattern = SimpleLinePattern.of(regularExpression);
}
/**
@@ -67,26 +68,25 @@ enum MdPattern
*
* @return the pattern
*/
- public Pattern getPattern()
+ public LinePattern getPattern()
{
return this.pattern;
}
- private static class PatternConstants
+ private static final class PatternConstants
{
- private PatternConstants()
- {
- // not instantiable
- }
-
public static final String ARTIFACT_TYPE = "[a-zA-Z]+";
public static final String BULLETS = "[+*-]";
private static final String UP_TO_3_WHITESPACES = "\\s{0,3}";
// [impl->dsn~md.requirement-references~1]
public static final String REFERENCE_AFTER_BULLET = UP_TO_3_WHITESPACES
+ PatternConstants.BULLETS + "(?:.*\\W)?" //
- + "((?:" + SpecificationItemId.ID_PATTERN + ")|(?:"
- + SpecificationItemId.LEGACY_ID_PATTERN + "))" //
+ + "(" + SpecificationItemId.ID_PATTERN + ")" //
+ "(?:\\W.*)?";
+
+ private PatternConstants()
+ {
+ // not instantiable
+ }
}
}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePattern.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePattern.java
new file mode 100644
index 000000000..6f915767c
--- /dev/null
+++ b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePattern.java
@@ -0,0 +1,31 @@
+package org.itsallcode.openfasttrace.importer.markdown;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LinePattern;
+
+class MdSectionTitlePattern implements LinePattern
+{
+ private static final LinePattern UNDERLINE = MdPattern.UNDERLINE.getPattern();
+ private static final LinePattern HASH_TITLE = MdPattern.TITLE.getPattern();
+
+ @Override
+ public Optional> getMatches(final String line, final String nextLine)
+ {
+ if (line == null)
+ {
+ return Optional.empty();
+ }
+ final Optional> hashTitle = HASH_TITLE.getMatches(line, null);
+ if (hashTitle.isPresent())
+ {
+ return hashTitle;
+ }
+ if (nextLine != null && UNDERLINE.getMatches(nextLine, null).isPresent())
+ {
+ return Optional.of(List.of(line));
+ }
+ return Optional.empty();
+ }
+}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/State.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/State.java
deleted file mode 100644
index 83066cfed..000000000
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/State.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-enum State
-{
- START, OUTSIDE, SPEC_ITEM, DESCRIPTION, COVERS, DEPENDS, RATIONALE, COMMENT, NEEDS, EOF, TITLE, TAGS
-}
\ No newline at end of file
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/Transition.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/Transition.java
deleted file mode 100644
index 343166771..000000000
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/Transition.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-class Transition
-{
- private final State from;
- private final State to;
- private final MdPattern markdownPattern;
- private final TransitionAction transitionAction;
-
- public Transition(final State from, final State to, final MdPattern markdownPattern,
- final TransitionAction transitionAction)
- {
- this.from = from;
- this.to = to;
- this.markdownPattern = markdownPattern;
- this.transitionAction = transitionAction;
- }
-
- public State getFrom()
- {
- return this.from;
- }
-
- public State getTo()
- {
- return this.to;
- }
-
- public MdPattern getMarkdownPattern()
- {
- return this.markdownPattern;
- }
-
- public TransitionAction getTransition()
- {
- return this.transitionAction;
- }
-
- @Override
- public String toString()
- {
- return "Transition [from=" + this.from + ", to=" + this.to + ", markdownPattern="
- + this.markdownPattern + "]";
- }
-}
diff --git a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/TransitionAction.java b/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/TransitionAction.java
deleted file mode 100644
index b1d801cf0..000000000
--- a/importer/markdown/src/main/java/org/itsallcode/openfasttrace/importer/markdown/TransitionAction.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-@FunctionalInterface
-interface TransitionAction
-{
- void transit();
-}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/ITMarkdownImporter.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/ITMarkdownImporter.java
deleted file mode 100644
index a9010c2cf..000000000
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/ITMarkdownImporter.java
+++ /dev/null
@@ -1,311 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.equalTo;
-import static org.itsallcode.openfasttrace.importer.markdown.MarkdownTestConstants.*;
-
-import java.io.BufferedReader;
-import java.io.StringReader;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.stream.Stream;
-
-import org.itsallcode.matcher.auto.AutoMatcher;
-import org.itsallcode.openfasttrace.api.core.*;
-import org.itsallcode.openfasttrace.api.importer.Importer;
-import org.itsallcode.openfasttrace.api.importer.SpecificationListBuilder;
-import org.itsallcode.openfasttrace.api.importer.input.InputFile;
-import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.junit.jupiter.params.provider.ValueSource;
-
-class ITMarkdownImporter
-{
- private static final String NL = System.lineSeparator();
- private static final String TAG2 = "Tag2";
- private static final String TAG1 = "Tag1";
- private static final String FILENAME = "file name";
-
- @Test
- void testFindRequirement()
- {
- assertThat(runImporterOnText(createCompleteSpecificationItemInMarkdownFormat()),
- AutoMatcher.contains(SpecificationItem.builder().id(ID1).title("Requirement Title")
- .comment("Comment" + NL + "More comment")
- .description("Description" + NL + NL + "More description")
- .rationale("Rationale" + NL + "More rationale")
- .addNeedsArtifactType("artA").addNeedsArtifactType("artB")
- .addCoveredId(SpecificationItemId.parseId(COVERED_ID1))
- .addCoveredId(SpecificationItemId.parseId(COVERED_ID2))
- .addDependOnId(SpecificationItemId.parseId(DEPENDS_ON_ID1))
- .addDependOnId(SpecificationItemId.parseId(DEPENDS_ON_ID2))
- .location("file name", 2)
- .build()));
-
- }
-
- // [utest->dsn~md.needs-coverage-list-compact~1]
- private String createCompleteSpecificationItemInMarkdownFormat()
- {
- return "# " + TITLE //
- + "\n" //
- + "`" + ID1 + "` " //
- + "\n" //
- + DESCRIPTION_LINE1 + "\n" //
- + DESCRIPTION_LINE2 + "\n" //
- + DESCRIPTION_LINE3 + "\n" //
- + "\nRationale:\n" //
- + RATIONALE_LINE1 + "\n" //
- + RATIONALE_LINE2 + "\n" //
- + "\nCovers:\n\n" //
- + " * " + COVERED_ID1 + "\n" //
- + " + " + "[Link to baz2](#" + COVERED_ID2 + ")\n" //
- + "\nDepends:\n\n" //
- + " + " + DEPENDS_ON_ID1 + "\n" //
- + " - " + DEPENDS_ON_ID2 + "\n" //
- + "\nComment:\n\n" //
- + COMMENT_LINE1 + "\n" //
- + COMMENT_LINE2 + "\n" //
- + "\nNeeds: " + NEEDS_ARTIFACT_TYPE1 //
- + " , " + NEEDS_ARTIFACT_TYPE2 + " ";
- }
-
- private List runImporterOnText(final String text)
- {
- final BufferedReader reader = new BufferedReader(new StringReader(text));
- final InputFile file = StreamInput.forReader(Paths.get(FILENAME), reader);
- final SpecificationListBuilder specItemBuilder = SpecificationListBuilder.create();
- final Importer importer = new MarkdownImporterFactory().createImporter(file,
- specItemBuilder);
- importer.runImport();
- return specItemBuilder.build();
- }
-
- @Test
- void testTwoConsecutiveSpecificationItems()
- {
- assertThat(runImporterOnText(createTwoConsecutiveItemsInMarkdownFormat()),
- AutoMatcher
- .contains(SpecificationItem.builder().id(ID1).title(TITLE).location("file name", 2).build(),
- SpecificationItem.builder().id(ID2).title("").location("file name", 4).build()));
- }
-
- private String createTwoConsecutiveItemsInMarkdownFormat()
- {
- return "# " + TITLE //
- + "\n" //
- + ID1 + "\n" //
- + "\n" + ID2 + "\n" //
- + "# Irrelevant Title";
- }
-
- @Test
- void testSingleNeeds()
- {
- final String singleNeedsItem = "`foo~bar~1`\n\nNeeds: " + NEEDS_ARTIFACT_TYPE1;
- final List items = runImporterOnText(singleNeedsItem);
- assertThat(items.get(0).getNeedsArtifactTypes(), contains(NEEDS_ARTIFACT_TYPE1));
- }
-
- @Test
- void testFindLegacyRequirement()
- {
- final String completeItem = createCompleteSpecificationItemInLegacyMarkdownFormat();
- assertThat(runImporterOnText(completeItem),
- AutoMatcher.contains(SpecificationItem.builder().id(SpecificationItemId.parseId(LEGACY_ID))
- .title("Requirement Title")
- .status(ItemStatus.PROPOSED)
- .comment("Comment" + NL + "More comment")
- .description("Description" + NL + NL + "More description")
- .rationale("Rationale" + NL + "More rationale")
- .addNeedsArtifactType("artA").addNeedsArtifactType("artB")
- .addCoveredId(SpecificationItemId.parseId(LEGACY_COVERED_ID1))
- .addCoveredId(SpecificationItemId.parseId(LEGACY_COVERED_ID2))
- .addDependOnId(SpecificationItemId.parseId(LEGACY_DEPENDS_ON_ID1))
- .addDependOnId(SpecificationItemId.parseId(LEGACY_DEPENDS_ON_ID2))
- .addTag("Tag1").addTag("Tag2")
- .location("file name", 2)
- .build()));
- }
-
- // [utest->dsn~md.needs-coverage-list~2]
- private String createCompleteSpecificationItemInLegacyMarkdownFormat()
- {
- return "# " + TITLE //
- + "\n" //
- + "`" + LEGACY_ID + "`" //
- + "\n" //
- + "\nStatus: proposed\n" //
- + "\nDescription:\n" + DESCRIPTION_LINE1 + "\n" //
- + DESCRIPTION_LINE2 + "\n" //
- + DESCRIPTION_LINE3 + "\n" //
- + "\nRationale:\n" //
- + RATIONALE_LINE1 + "\n" //
- + RATIONALE_LINE2 + "\n" //
- + "\nDepends:\n\n" //
- + " + `" + LEGACY_DEPENDS_ON_ID1 + "`\n" //
- + " - `" + LEGACY_DEPENDS_ON_ID2 + "`\n" //
- + "\nCovers:\n\n" //
- + " * `" + LEGACY_COVERED_ID1 + "`\n" //
- + " + `" + LEGACY_COVERED_ID2 + "`\n" //
- + "\nComment:\n\n" //
- + COMMENT_LINE1 + "\n" //
- + COMMENT_LINE2 + "\n" //
- + "\nNeeds:\n" //
- + " * " + NEEDS_ARTIFACT_TYPE1 + "\n"//
- + "+ " + NEEDS_ARTIFACT_TYPE2 + "\n" //
- + "\nTags: " + TAG1 + ", " + TAG2;
- }
-
- // [utest->dsn~md.artifact-forwarding-notation~1]
- @Test
- void testForwardRequirement()
- {
- final List items = runImporterOnText("arch-->dsn:req~foobar~2\n" //
- + " * `dsn --> impl, utest,itest : arch~bar.zoo~123`");
- assertThat(items,
- AutoMatcher.contains(SpecificationItem.builder().id(SpecificationItemId.parseId("arch~foobar~2"))
- .forwards(true)
- .addCoveredId(SpecificationItemId.parseId("req~foobar~2"))
- .addNeedsArtifactType("dsn")
- .build(),
- SpecificationItem.builder().id(SpecificationItemId.parseId("dsn~bar.zoo~123"))
- .addCoveredId(SpecificationItemId.parseId("arch~bar.zoo~123"))
- .addNeedsArtifactType("impl").addNeedsArtifactType("utest")
- .addNeedsArtifactType("itest")
- .forwards(true)
- .build()));
- }
-
- // [utest->dsn~md.specification-item-title~1]
- @Test
- void testFindTitleAfterTitle()
- {
- assertThat(runImporterOnText("## This title should be ignored\n\n" //
- + "### Title\n" //
- + "`a~b~1`"),
- AutoMatcher.contains(SpecificationItem.builder().id(SpecificationItemId.parseId("a~b~1"))
- .title("Title").location("file name", 4)
- .build()));
- }
-
- @ParameterizedTest
- @MethodSource("needsCoverage")
- void testNeedsCoverage(final String mdContent, final List expected)
- {
- final List items = runImporterOnText("`a~b~1`\n" + mdContent);
- assertThat(items.get(0).getNeedsArtifactTypes(), equalTo(expected));
- }
-
- static Stream needsCoverage()
- {
- return Stream.of(
- Arguments.of("Needs: req , dsn ", List.of("req", "dsn")),
- Arguments.of("Needs: req ", List.of("req")),
- Arguments.of("Needs: req,dsn ", List.of("req", "dsn")),
- Arguments.of("Needs: req ,dsn", List.of("req", "dsn")),
- Arguments.of("Needs: req,dsn", List.of("req", "dsn")),
- Arguments.of("Needs: req,\tdsn\n", List.of("req", "dsn")),
- Arguments.of("Needs:req,dsn", List.of("req", "dsn")),
- Arguments.of("Needs:\n* req\n* dsn", List.of("req", "dsn")),
- Arguments.of("Needs:\n * req\n * dsn", List.of("req", "dsn")),
- Arguments.of("Needs:\n* req \n\t* dsn ", List.of("req", "dsn")),
- Arguments.of("Needs:\n* req\n* dsn", List.of("req", "dsn")));
- }
-
- @ParameterizedTest
- @MethodSource("tags")
- void testTags(final String mdContent, final List expected)
- {
- final List items = runImporterOnText("`a~b~1`\n" + mdContent);
- assertThat(items.get(0).getTags(), equalTo(expected));
- }
-
- static Stream tags()
- {
- return Stream.of(
- Arguments.of("Tags: req , dsn ", List.of("req", "dsn")),
- Arguments.of("Tags: req ", List.of("req")),
- Arguments.of("Tags: req,dsn ", List.of("req", "dsn")),
- Arguments.of("Tags: req ,dsn", List.of("req", "dsn")),
- Arguments.of("Tags: req,dsn", List.of("req", "dsn")),
- Arguments.of("Tags: req,\tdsn\n", List.of("req", "dsn")),
- Arguments.of("Tags:req,dsn", List.of("req", "dsn")),
- Arguments.of("Tags:\n* req\n* dsn", List.of("req", "dsn")),
- Arguments.of("Tags:\n * req\n * dsn\n", List.of("req", "dsn")),
- Arguments.of("Tags:\n* req \n\t* dsn ", List.of("req", "dsn")),
- Arguments.of("Tags:\n* req\n* dsn", List.of("req", "dsn")));
- }
-
- @Test
- void testItemIdSupportsUmlauts()
- {
- final List items = runImporterOnText(
- "`### Die Implementierung muss den Zustand einzelner Zellen ändern.\n"
- + "`req~zellzustandsänderung~1`\n"
- + "Diese Anforderung ermöglicht die Aktualisierung des Zustands von lebenden und toten Zellen"
- + " in jeder Generation.\n"
- + "Needs: arch");
- assertThat(items, AutoMatcher.contains(SpecificationItem.builder()
- .id(SpecificationItemId.createId("req", "zellzustandsänderung", 1))
- .description(
- "Diese Anforderung ermöglicht die Aktualisierung des Zustands von lebenden und toten Zellen"
- + " in jeder Generation.")
- .location("file name", 2).addNeedsArtifactType("arch")
- .build()));
- }
-
- @ParameterizedTest
- @ValueSource(strings =
- { "---------------------------------", "---", "===", "======" })
- void testRecognizeItemTitleWithUnderlines(final String underline)
- {
- final List items = runImporterOnText(
- "This is a title with an underline\n" //
- + underline + "\n" //
- + "`extra~support-underlined-headers~1`\n" //
- + "Body text.\n");
- assertThat(items, AutoMatcher.contains(SpecificationItem.builder()
- .id(SpecificationItemId.createId("extra", "support-underlined-headers", 1))
- .title("This is a title with an underline")
- .description("Body text.")
- .location("file name", 3)
- .build()));
- }
-
- @ParameterizedTest
- @ValueSource(strings =
- { "---------------------------------", "---", "===", "======", "================================================" })
- void testRecognizeItemTitleWithUnderlinesAfterAnotherTitle(final String underline)
- {
- final List items = runImporterOnText(
- "# This must be ignored.\n" //
- + "This is a title with an underline\n" //
- + underline + "\n" //
- + "`extra~support-underlined-headers~1`\n" //
- + "Body text.\n");
- assertThat(items, AutoMatcher.contains(SpecificationItem.builder()
- .id(SpecificationItemId.createId("extra", "support-underlined-headers", 1))
- .title("This is a title with an underline")
- .description("Body text.")
- .location("file name", 4)
- .build()));
- }
-
- @Test
- void testLessThenTwoUnderliningCharactersAreNotDetectedAsTitleUnderlines()
- {
- final List items = runImporterOnText(
- "This is not a title since the underline is too short\n"
- + "--\n"
- + "req~too-short~111");
- assertThat(items, AutoMatcher.contains(SpecificationItem.builder()
- .id(SpecificationItemId.createId("req", "too-short", 111))
- .location("file name", 3)
- .build()));
- }
-}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownAsserts.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownAsserts.java
index 43cbf74da..2c8815396 100644
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownAsserts.java
+++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownAsserts.java
@@ -1,10 +1,10 @@
package org.itsallcode.openfasttrace.importer.markdown;
import static org.hamcrest.MatcherAssert.assertThat;
-
import static org.hamcrest.Matchers.equalTo;
-import java.util.regex.Matcher;
+import java.util.List;
+import java.util.Optional;
class MarkdownAsserts
{
@@ -23,9 +23,9 @@ static void assertMatching(final String[] samples, final MdPattern mdPattern,
{
for (final String text : samples)
{
- final Matcher matcher = mdPattern.getPattern().matcher(text);
+ final Optional> matcher = mdPattern.getPattern().getMatches(text, null);
assertThat(mdPattern.toString() + " must " + (mustMatch ? "" : "not ") + "match " + "\""
- + text + "\"", matcher.matches(), equalTo(mustMatch));
+ + text + "\"", matcher.isPresent(), equalTo(mustMatch));
}
}
}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownTestConstants.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownTestConstants.java
index 63f5ac330..8a24c124f 100644
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownTestConstants.java
+++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MarkdownTestConstants.java
@@ -9,22 +9,7 @@ private MarkdownTestConstants()
// not instantiable
}
- static final SpecificationItemId ID1 = SpecificationItemId.parseId("type~id~1");
static final SpecificationItemId ID2 = SpecificationItemId.parseId("type~id~2");
- static final String TITLE = "Requirement Title";
- static final String DESCRIPTION_LINE1 = "Description";
- static final String DESCRIPTION_LINE2 = "";
- static final String DESCRIPTION_LINE3 = "More description";
- static final String RATIONALE_LINE1 = "Rationale";
- static final String RATIONALE_LINE2 = "More rationale";
- static final String COMMENT_LINE1 = "Comment";
- static final String COMMENT_LINE2 = "More comment";
- static final String COVERED_ID1 = "impl~foo1~1";
- static final String COVERED_ID2 = "impl~baz2~2";
- static final String NEEDS_ARTIFACT_TYPE1 = "artA";
- static final String NEEDS_ARTIFACT_TYPE2 = "artB";
- static final String DEPENDS_ON_ID1 = "configuration~blubb.blah.blah~4711";
- static final String DEPENDS_ON_ID2 = "db~blah.blubb~42";
// Legacy Markdown format
static final String LEGACY_ID = "type~type:legacy_id, v3";
static final String LEGACY_COVERED_ID1 = "impl:legacy_foo1, v3";
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePatternTest.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePatternTest.java
new file mode 100644
index 000000000..33c73e67d
--- /dev/null
+++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/MdSectionTitlePatternTest.java
@@ -0,0 +1,81 @@
+package org.itsallcode.openfasttrace.importer.markdown;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class MdSectionTitlePatternTest
+{
+ static Stream testCases()
+ {
+ return Stream.of(
+ titleNotRecongnized(null, null),
+ titleNotRecongnized(null, "ignored"),
+ titleNotRecongnized(null, "===="),
+ titleNotRecongnized("ignored", null),
+ testCase("# Title", null, "Title"),
+ testCase("## Title", null, "Title"),
+ testCase("## Title with words", null, "Title with words"),
+ testCase("## \tLeading whitespace removed ", null, "Leading whitespace removed "),
+ testCase("# Title", "ignored", "Title"),
+ testCase("# Title", "=======", "Title"),
+ testCase("Title with words", "=======", "Title with words"),
+ testCase("\t Leading & trailing whitespace not removed ", "=======",
+ "\t Leading & trailing whitespace not removed "),
+ underlineRecognized("======="),
+ underlineRecognized("-------"),
+ underlineRecognized("==="),
+ underlineRecognized("---"),
+ underlineNotRecognized("=="),
+ underlineNotRecognized("--"),
+ underlineNotRecognized("______"),
+ underlineNotRecognized("^^^^^^"));
+ }
+
+ private static Arguments underlineRecognized(final String underline)
+ {
+ return Arguments.of("Title", underline, "Title");
+ }
+
+ private static Arguments underlineNotRecognized(final String underline)
+ {
+ return Arguments.of("Title", underline, null);
+ }
+
+ private static Arguments titleNotRecongnized(final String line, final String nextLine)
+ {
+ return testCase(line, nextLine, null);
+ }
+
+ private static Arguments testCase(final String line, final String nextLine, final String expected)
+ {
+ return Arguments.of(line, nextLine, expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("testCases")
+ void test(final String line, final String nextLine, final String expected)
+ {
+ final MdSectionTitlePattern pattern = new MdSectionTitlePattern();
+ final Optional> result = pattern.getMatches(line, nextLine);
+ if (expected == null)
+ {
+ assertThat("Lines '" + line + "' + '" + nextLine + "' should not be recognized as a section title",
+ result.isPresent(), is(false));
+ }
+ else
+ {
+ assertAll(() -> assertThat(
+ "Lines '" + line + "' + '" + nextLine + "' should be recognized as a section title",
+ result.isPresent(), is(true)), () -> assertThat(result.get().get(0), is(expected)));
+ }
+ }
+}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java
index fdd3a2af4..8f8478927 100644
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java
+++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownImporter.java
@@ -2,37 +2,25 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
-import static org.itsallcode.openfasttrace.importer.markdown.MarkdownAsserts.assertMatch;
-import static org.itsallcode.openfasttrace.importer.markdown.MarkdownAsserts.assertMismatch;
-import static org.itsallcode.openfasttrace.importer.markdown.MarkdownTestConstants.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.when;
-import java.io.*;
-import java.nio.file.Paths;
+import java.io.BufferedReader;
+import java.io.IOException;
-import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
-import org.itsallcode.openfasttrace.api.importer.Importer;
import org.itsallcode.openfasttrace.api.importer.ImporterException;
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
-import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
-import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class TestMarkdownImporter
{
- private static final String FILENAME = "file name";
-
- @Mock
- ImportEventListener listenerMock;
-
// [utest->dsn~md.specification-item-id-format~3]
@ParameterizedTest
@CsvSource(
@@ -44,7 +32,7 @@ class TestMarkdownImporter
})
void testIdentifyId(final String text)
{
- assertMatch(MdPattern.ID, text);
+ MarkdownAsserts.assertMatch(MdPattern.ID, text);
}
// [utest->dsn~md.specification-item-id-format~3]
@@ -53,7 +41,7 @@ void testIdentifyId(final String text)
{ "test~1", "req-test~1", "req~4test~1", "räq~test~1" })
void testIdentifyNonId(final String text)
{
- assertMismatch(MdPattern.ID, text);
+ MarkdownAsserts.assertMismatch(MdPattern.ID, text);
}
// [utest->dsn~md.specification-item-title~1]
@@ -62,7 +50,7 @@ void testIdentifyNonId(final String text)
{ "#Title", "# Title", "###### Title", "# Title", "# Änderung" })
void testIdentifyTitle(final String text)
{
- assertMatch(MdPattern.TITLE, text);
+ MarkdownAsserts.assertMatch(MdPattern.TITLE, text);
}
// [utest->dsn~md.specification-item-title~1]
@@ -71,7 +59,7 @@ void testIdentifyTitle(final String text)
{ "Title", "Title #", "' # Title'" })
void testIdentifyNonTitle(final String text)
{
- assertMismatch(MdPattern.TITLE, text);
+ MarkdownAsserts.assertMismatch(MdPattern.TITLE, text);
}
@ParameterizedTest
@@ -79,7 +67,7 @@ void testIdentifyNonTitle(final String text)
{ "Needs: req, dsn", "Needs:req,dsn", "'Needs: \treq , dsn '" })
void testIdentifyNeeds(final String text)
{
- assertMatch(MdPattern.NEEDS_INT, text);
+ MarkdownAsserts.assertMatch(MdPattern.NEEDS_INT, text);
}
@ParameterizedTest
@@ -87,7 +75,7 @@ void testIdentifyNeeds(final String text)
{ "Needs:", "#Needs: abc", "' Needs: abc'", "Needs: önderung" })
void testIdentifyNonNeeds(final String text)
{
- assertMismatch(MdPattern.NEEDS_INT, text);
+ MarkdownAsserts.assertMismatch(MdPattern.NEEDS_INT, text);
}
@ParameterizedTest
@@ -95,7 +83,7 @@ void testIdentifyNonNeeds(final String text)
{ "Tags: req, dsn", "Tags:req,dsn", "'Tags: \treq , dsn '" })
void testIdentifyTags(final String text)
{
- assertMatch(MdPattern.TAGS_INT, text);
+ MarkdownAsserts.assertMatch(MdPattern.TAGS_INT, text);
}
@ParameterizedTest
@@ -103,140 +91,18 @@ void testIdentifyTags(final String text)
{ "Tags:", "#Needs: abc", "' Needs: abc'", "Needs: änderung" })
void testIdentifyNonTags(final String text)
{
- assertMismatch(MdPattern.TAGS_INT, text);
- }
-
- @Test
- void testFindRequirement()
- {
- final String completeItem = createCompleteSpecificationItemInMarkdownFormat();
- runImporterOnText(completeItem);
- assertAllImporterEventsCalled();
- }
-
- // [utest->dsn~md.needs-coverage-list-compact~1]
- private String createCompleteSpecificationItemInMarkdownFormat()
- {
- return "# " + TITLE //
- + "\n" //
- + "`" + ID1 + "` " //
- + "\n" //
- + DESCRIPTION_LINE1 + "\n" //
- + DESCRIPTION_LINE2 + "\n" //
- + DESCRIPTION_LINE3 + "\n" //
- + "\nRationale:\n" //
- + RATIONALE_LINE1 + "\n" //
- + RATIONALE_LINE2 + "\n" //
- + "\nCovers:\n\n" //
- + " * " + COVERED_ID1 + "\n" //
- + " + " + "[Link to baz2](#" + COVERED_ID2 + ")\n" //
- + "\nDepends:\n\n" //
- + " + " + DEPENDS_ON_ID1 + "\n" //
- + " - " + DEPENDS_ON_ID2 + "\n" //
- + "\nComment:\n\n" //
- + COMMENT_LINE1 + "\n" //
- + COMMENT_LINE2 + "\n" //
- + "\nNeeds: " + NEEDS_ARTIFACT_TYPE1 //
- + " , " + NEEDS_ARTIFACT_TYPE2 + " ";
- }
-
- private void runImporterOnText(final String text)
- {
- final BufferedReader reader = new BufferedReader(new StringReader(text));
- final InputFile file = StreamInput.forReader(Paths.get(FILENAME), reader);
- final Importer importer = new MarkdownImporterFactory().createImporter(file,
- this.listenerMock);
- importer.runImport();
- }
-
- // [utest->dsn~md.covers-list~1]
- // [utest->dsn~md.depends-list~1]
- // [utest->dsn~md.requirement-references~1]
- private void assertAllImporterEventsCalled()
- {
- final InOrder inOrder = inOrder(this.listenerMock);
- inOrder.verify(this.listenerMock).beginSpecificationItem();
- inOrder.verify(this.listenerMock).setId(ID1);
- inOrder.verify(this.listenerMock).setLocation(FILENAME, 2);
- inOrder.verify(this.listenerMock).setTitle(TITLE);
- inOrder.verify(this.listenerMock).appendDescription(DESCRIPTION_LINE1);
- inOrder.verify(this.listenerMock).appendDescription(System.lineSeparator());
- inOrder.verify(this.listenerMock).appendDescription(DESCRIPTION_LINE2);
- inOrder.verify(this.listenerMock).appendDescription(System.lineSeparator());
- inOrder.verify(this.listenerMock).appendDescription(DESCRIPTION_LINE3);
- inOrder.verify(this.listenerMock).appendRationale(RATIONALE_LINE1);
- inOrder.verify(this.listenerMock).appendRationale(RATIONALE_LINE2);
- inOrder.verify(this.listenerMock).addCoveredId(SpecificationItemId.parseId(COVERED_ID1));
- inOrder.verify(this.listenerMock).addCoveredId(SpecificationItemId.parseId(COVERED_ID2));
- inOrder.verify(this.listenerMock)
- .addDependsOnId(SpecificationItemId.parseId(MarkdownTestConstants.DEPENDS_ON_ID1));
- inOrder.verify(this.listenerMock)
- .addDependsOnId(SpecificationItemId.parseId(DEPENDS_ON_ID2));
- inOrder.verify(this.listenerMock).appendComment(COMMENT_LINE1);
- inOrder.verify(this.listenerMock).appendComment(COMMENT_LINE2);
- inOrder.verify(this.listenerMock).addNeededArtifactType(NEEDS_ARTIFACT_TYPE1);
- inOrder.verify(this.listenerMock).addNeededArtifactType(NEEDS_ARTIFACT_TYPE2);
- inOrder.verify(this.listenerMock).endSpecificationItem();
- inOrder.verifyNoMoreInteractions();
- }
-
- @Test
- void testTwoConsecutiveSpecificationItems()
- {
- runImporterOnText(createTwoConsecutiveItemsInMarkdownFormat());
- assertImporterEventsForTwoConsecutiveItemsCalled();
- }
-
- private String createTwoConsecutiveItemsInMarkdownFormat()
- {
- return "# " + TITLE //
- + "\n" //
- + ID1 + "\n" //
- + "\n" + ID2 + "\n" //
- + "# Irrelevant Title";
- }
-
- private void assertImporterEventsForTwoConsecutiveItemsCalled()
- {
- final InOrder inOrder = inOrder(this.listenerMock);
- inOrder.verify(this.listenerMock).beginSpecificationItem();
- inOrder.verify(this.listenerMock).setId(ID1);
- inOrder.verify(this.listenerMock).setLocation(FILENAME, 2);
- inOrder.verify(this.listenerMock).setTitle(TITLE);
- inOrder.verify(this.listenerMock).endSpecificationItem();
- inOrder.verify(this.listenerMock).beginSpecificationItem();
- inOrder.verify(this.listenerMock).setId(ID2);
- inOrder.verify(this.listenerMock).setLocation(FILENAME, 4);
- inOrder.verify(this.listenerMock).endSpecificationItem();
- inOrder.verifyNoMoreInteractions();
- }
-
- @Test
- void testSingleNeeds()
- {
- final String singleNeedsItem = "`foo~bar~1`\n\nNeeds: " + NEEDS_ARTIFACT_TYPE1;
- runImporterOnText(singleNeedsItem);
- verify(this.listenerMock, times(1)).addNeededArtifactType(NEEDS_ARTIFACT_TYPE1);
- }
-
- // [utest->dsn~md.eb-markdown-id~1]
- @Test
- void testIdentifyLegacyId()
- {
- assertMatch(MdPattern.ID, "a:b, v0", "req:test, v1", "req:test,v1", "req:test, v999",
- "req:test.requirement, v1", "req:test_underscore, v1",
- "`req:test1, v1`arbitrary text");
- assertMismatch(MdPattern.ID, "test, v1", "req-test, v1", "req.4test, v1");
+ MarkdownAsserts.assertMismatch(MdPattern.TAGS_INT, text);
}
@Test
void testRunImportHandlesIOException(@Mock final InputFile fileMock, @Mock final ImportEventListener listenerMock,
- @Mock final BufferedReader readerMock) throws IOException {
+ @Mock final BufferedReader readerMock) throws IOException
+ {
when(fileMock.getPath()).thenReturn("/the/file");
when(fileMock.createReader()).thenReturn(readerMock);
when(readerMock.readLine()).thenThrow(new IOException("Dummy exception"));
final MarkdownImporter importer = new MarkdownImporter(fileMock, listenerMock);
final ImporterException exception = assertThrows(ImporterException.class, importer::runImport);
- assertThat(exception.getMessage(), equalTo("Error reading \"/the/file\" at line 0"));
+ assertThat(exception.getMessage(), equalTo("Error reading '/the/file' at line 0: Dummy exception"));
}
}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java
new file mode 100644
index 000000000..2f6595cc1
--- /dev/null
+++ b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TestMarkdownMarkupImporter.java
@@ -0,0 +1,141 @@
+package org.itsallcode.openfasttrace.importer.markdown;
+
+import static org.itsallcode.matcher.auto.AutoMatcher.contains;
+import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item;
+
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.itsallcode.openfasttrace.api.importer.ImporterFactory;
+import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestMarkdownMarkupImporter extends AbstractLightWeightMarkupImporterTest
+{
+ private final static ImporterFactory importerFactory = new MarkdownImporterFactory();
+
+ TestMarkdownMarkupImporter()
+ {
+ super(0);
+ }
+
+ @Override
+ protected ImporterFactory getImporterFactory()
+ {
+ return importerFactory;
+ }
+
+ protected String formatTitle(final String title, final int level)
+ {
+ return "#".repeat(level) + " " + title;
+ }
+
+ // [utest -> dsn~md.specification-item-title~1]
+ @Test
+ void testMarkdownTitleBeforeRequirementIdIsRequirementTitle()
+ {
+ assertImport("titles.md",
+ """
+ # The title
+ the~id~1
+ """,
+ contains(item()
+ .id("the", "id", 1)
+ .title("The title")
+ .location("titles.md", 2)
+ .build()));
+ }
+
+ // [utest -> dsn~md.specification-item-title~1]
+ @Test
+ void testMarkdownTitleDetectedAfterAnotherTitle()
+ {
+ assertImport("more_titles.md",
+ """
+ # 1st level title
+
+ # 2nd level title
+
+ the~id~1
+ """,
+ contains(item()
+ .title("2nd level title")
+ .id("the", "id", 1)
+ .location("more_titles.md", 5)
+ .build()));
+ }
+
+ // [utest->dsn~md.specification-item-title~1]
+ @Test
+ void testFindTitleAfterTitle()
+ {
+ assertImport("x", """
+ ## This title should be ignored
+
+ ### Title
+ `a~b~1
+ """,
+ contains(item()
+ .id(SpecificationItemId.parseId("a~b~1"))
+ .title("Title").location("x", 4)
+ .build()));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "---------------------------------", "---", "===", "======", "--- ",
+ "=== ", "---\t" })
+ void testRecognizeItemTitleWithUnderlines(final String underline)
+ {
+ assertImport("file name", """
+ This is a title with an underline
+ %s
+ `extra~support-underlined-headers~1`
+ Body text.
+ """.formatted(underline),
+ contains(item()
+ .id(SpecificationItemId.createId("extra", "support-underlined-headers",
+ 1))
+ .title("This is a title with an underline")
+ .description("Body text.")
+ .location("file name", 3)
+ .build()));
+ }
+
+ @ValueSource(strings = { "---------------------------------", "---", "===", "======",
+ "================================================",
+ "--- ", "=== ", "---\t"
+ })
+ @ParameterizedTest
+ void testRecognizeItemTitleWithUnderlinesAfterAnotherTitle(final String underline)
+ {
+ assertImport("y", """
+ # This must be ignored.
+ This is a title with an underline
+ %s
+ `extra~support-underlined-headers~1`
+ Body text.
+ """.formatted(underline),
+ contains(item()
+ .id(SpecificationItemId.createId("extra", "support-underlined-headers",
+ 1))
+ .title("This is a title with an underline")
+ .description("Body text.")
+ .location("y", 4)
+ .build()));
+ }
+
+ @Test
+ void testLessThenThreeUnderliningCharactersAreNotDetectedAsTitleUnderlines()
+ {
+ assertImport("z", """
+ This is not a title since the underline is too short
+ --
+ req~too-short~111
+ """,
+ contains(item()
+ .id(SpecificationItemId.createId("req", "too-short", 111))
+ .location("z", 3)
+ .build()));
+ }
+}
diff --git a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TransitionTest.java b/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TransitionTest.java
deleted file mode 100644
index 20dfd8af4..000000000
--- a/importer/markdown/src/test/java/org/itsallcode/openfasttrace/importer/markdown/TransitionTest.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.itsallcode.openfasttrace.importer.markdown;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-
-@ExtendWith(MockitoExtension.class)
-class TransitionTest
-{
- @Test
- void testToString(@Mock final TransitionAction actionMock)
- {
- final Transition transition = new Transition(State.OUTSIDE, State.TITLE, MdPattern.TITLE, actionMock);
- assertThat(transition.toString(), equalTo("Transition [from=OUTSIDE, to=TITLE, markdownPattern=TITLE]"));
- }
-}
\ No newline at end of file
diff --git a/importer/markdown/src/test/resources/logging.properties b/importer/markdown/src/test/resources/logging.properties
new file mode 100644
index 000000000..b2c9cbc7e
--- /dev/null
+++ b/importer/markdown/src/test/resources/logging.properties
@@ -0,0 +1,11 @@
+handlers = java.util.logging.ConsoleHandler org.itsallcode.openfasttrace.testutil.log.NoOpLoggingHandler
+.level = INFO
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = org.itsallcode.openfasttrace.testutil.log.ShortClassNameFormatter
+java.util.logging.ConsoleHandler.encoding = UTF-8
+
+org.itsallcode.openfasttrace.testutil.log.NoOpLoggingHandler.level = ALL
+
+# Set this to FINEST for debugging the state machine
+org.itsallcode.openfasttrace.testutil.level = FINE
+org.itsallcode.openfasttrace.importer.level = FINE
diff --git a/importer/restructuredtext/.settings/org.eclipse.jdt.core.prefs b/importer/restructuredtext/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..a40522564
--- /dev/null
+++ b/importer/restructuredtext/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,413 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=warning
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.processAnnotations=disabled
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=17
+org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
+org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false
+org.eclipse.jdt.core.formatter.align_with_spaces=false
+org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=49
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0
+org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0
+org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_module_statements=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_record_components=16
+org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0
+org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0
+org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=next_line_on_wrap
+org.eclipse.jdt.core.formatter.brace_position_for_block=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=next_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=next_line
+org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false
+org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=false
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.indent_tag_description=false
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=false
+org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.lineSplit=120
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.text_block_indentation=0
+org.eclipse.jdt.core.formatter.use_on_off_tags=true
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true
+org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
+org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/importer/restructuredtext/.settings/org.eclipse.jdt.ui.prefs b/importer/restructuredtext/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 000000000..0a69ece30
--- /dev/null
+++ b/importer/restructuredtext/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,146 @@
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_itsallcode style
+formatter_settings_version=21
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=java;javax;org;com;
+org.eclipse.jdt.ui.ondemandthreshold=4
+org.eclipse.jdt.ui.staticondemandthreshold=4
+sp_cleanup.add_all=false
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.array_with_curly=false
+sp_cleanup.arrays_fill=false
+sp_cleanup.bitwise_conditional_expression=false
+sp_cleanup.boolean_literal=false
+sp_cleanup.boolean_value_rather_than_comparison=false
+sp_cleanup.break_loop=false
+sp_cleanup.collection_cloning=false
+sp_cleanup.comparing_on_criteria=false
+sp_cleanup.comparison_statement=false
+sp_cleanup.controlflow_merge=false
+sp_cleanup.convert_functional_interfaces=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false
+sp_cleanup.convert_to_switch_expressions=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.do_while_rather_than_while=false
+sp_cleanup.double_negation=false
+sp_cleanup.else_if=false
+sp_cleanup.embedded_if=false
+sp_cleanup.evaluate_nullable=false
+sp_cleanup.extract_increment=false
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.hash=false
+sp_cleanup.if_condition=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.instanceof=false
+sp_cleanup.instanceof_keyword=false
+sp_cleanup.invert_equals=false
+sp_cleanup.join=false
+sp_cleanup.lazy_logical_operator=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=true
+sp_cleanup.map_cloning=false
+sp_cleanup.merge_conditional_blocks=false
+sp_cleanup.multi_catch=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.no_string_creation=false
+sp_cleanup.no_super=false
+sp_cleanup.number_suffix=false
+sp_cleanup.objects_equals=false
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false
+sp_cleanup.operand_factorization=false
+sp_cleanup.organize_imports=true
+sp_cleanup.overridden_assignment=false
+sp_cleanup.plain_replacement=false
+sp_cleanup.precompile_regex=false
+sp_cleanup.primitive_comparison=false
+sp_cleanup.primitive_parsing=false
+sp_cleanup.primitive_rather_than_wrapper=false
+sp_cleanup.primitive_serialization=false
+sp_cleanup.pull_out_if_from_if_else=false
+sp_cleanup.pull_up_assignment=false
+sp_cleanup.push_down_negation=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.reduce_indentation=false
+sp_cleanup.redundant_comparator=false
+sp_cleanup.redundant_falling_through_block_end=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_modifiers=false
+sp_cleanup.remove_redundant_semicolons=false
+sp_cleanup.remove_redundant_type_arguments=false
+sp_cleanup.remove_trailing_whitespaces=false
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_array_creation=false
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.return_expression=false
+sp_cleanup.simplify_lambda_expression_and_method_ref=false
+sp_cleanup.single_used_field=false
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.standard_comparison=false
+sp_cleanup.static_inner_class=false
+sp_cleanup.strictly_equal_or_different=false
+sp_cleanup.stringbuffer_to_stringbuilder=false
+sp_cleanup.stringbuilder=false
+sp_cleanup.stringbuilder_for_local_vars=false
+sp_cleanup.stringconcat_to_textblock=false
+sp_cleanup.substring=false
+sp_cleanup.switch=false
+sp_cleanup.system_property=false
+sp_cleanup.system_property_boolean=false
+sp_cleanup.system_property_file_encoding=false
+sp_cleanup.system_property_file_separator=false
+sp_cleanup.system_property_line_separator=false
+sp_cleanup.system_property_path_separator=false
+sp_cleanup.ternary_operator=false
+sp_cleanup.try_with_resource=false
+sp_cleanup.unlooped_while=false
+sp_cleanup.unreachable_block=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_autoboxing=false
+sp_cleanup.use_blocks=true
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_directly_map_method=false
+sp_cleanup.use_lambda=true
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_string_is_blank=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_unboxing=false
+sp_cleanup.use_var=false
+sp_cleanup.useless_continue=false
+sp_cleanup.useless_return=false
+sp_cleanup.valueof_rather_than_instantiation=false
diff --git a/importer/restructuredtext/pom.xml b/importer/restructuredtext/pom.xml
new file mode 100644
index 000000000..f3fd4cfe7
--- /dev/null
+++ b/importer/restructuredtext/pom.xml
@@ -0,0 +1,31 @@
+
+ 4.0.0
+ openfasttrace-importer-restructuredtext
+ OpenFastTrace reStructuredText Importer
+
+ ../../parent/pom.xml
+ org.itsallcode.openfasttrace
+ openfasttrace-parent
+ ${revision}
+
+
+ ${reproducible.build.timestamp}
+
+
+
+ org.itsallcode.openfasttrace
+ openfasttrace-api
+
+
+ org.itsallcode.openfasttrace
+ openfasttrace-importer-lightweightmarkup
+
+
+ org.itsallcode.openfasttrace
+ openfasttrace-testutil
+ test
+
+
+
diff --git a/importer/restructuredtext/src/main/java/module-info.java b/importer/restructuredtext/src/main/java/module-info.java
new file mode 100644
index 000000000..4892618c3
--- /dev/null
+++ b/importer/restructuredtext/src/main/java/module-info.java
@@ -0,0 +1,15 @@
+import org.itsallcode.openfasttrace.importer.restructuredtext.RestructuredTextImporterFactory;
+
+/**
+ * This provides an importer for the reStructuredText format.
+ *
+ * @provides org.itsallcode.openfasttrace.api.importer.ImporterFactory
+ */
+module org.itsallcode.openfasttrace.importer.restructuredtext
+{
+ requires transitive org.itsallcode.openfasttrace.api;
+ requires org.itsallcode.openfasttrace.importer.lightweightmarkup;
+
+ provides org.itsallcode.openfasttrace.api.importer.ImporterFactory
+ with RestructuredTextImporterFactory;
+}
diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java
new file mode 100644
index 000000000..e6381f76d
--- /dev/null
+++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporter.java
@@ -0,0 +1,172 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import static org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LineParserState.*;
+
+import org.itsallcode.openfasttrace.api.importer.ImportEventListener;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.LightWeightMarkupImporter;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.*;
+
+/**
+ * Importer for OFT augmented reStructuredText.
+ *
+ * The purpose of this importer is to find specification items that follow a
+ * certain structure inside reStructuredText documents. It is not the goal to
+ * ingest the complete reStructuredText document though, only the specification
+ * items. Also, the hierarchical structure of the document itself has no impact
+ * on the specification items. OFT just extracts a flat list. Linking the items
+ * is explicitly not the purpose of the importer.
+ *
+ */
+public class RestructuredTextImporter extends LightWeightMarkupImporter
+{
+ private static final LinePattern SECTION_TITLE = new RstSectionTitlePattern();
+
+ /**
+ * Creates a {@link RestructuredTextImporter} object with the given
+ * parameters.
+ *
+ * @param fileName
+ * the input file to be imported
+ * @param listener
+ * the listener to handle import events
+ */
+ RestructuredTextImporter(final InputFile fileName, final ImportEventListener listener)
+ {
+ super(fileName, listener);
+ }
+
+ @Override
+ protected Transition[] configureTransitions()
+ {
+ // @formatter:off
+ return new Transition[]{
+ transition(START , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(START , START , RstPattern.FORWARD , this::forward ),
+ transition(START , TITLE , SECTION_TITLE , this::rememberTitle ),
+ transition(START , START , RstPattern.EVERYTHING , () -> {} ),
+
+ transition(TITLE , TITLE , RstPattern.UNDERLINE , () -> {} ),
+ transition(TITLE , TITLE , SECTION_TITLE , this::rememberTitle ),
+ transition(TITLE , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(TITLE , TITLE , RstPattern.EMPTY , () -> {} ),
+ transition(TITLE , START , RstPattern.FORWARD , () -> {endItem(); forward();} ),
+ transition(TITLE , START , RstPattern.EVERYTHING , this::resetTitle ),
+
+ transition(SPEC_ITEM , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(SPEC_ITEM , SPEC_ITEM , RstPattern.STATUS , this::setStatus ),
+ transition(SPEC_ITEM , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(SPEC_ITEM , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(SPEC_ITEM , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(SPEC_ITEM , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(SPEC_ITEM , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(SPEC_ITEM , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(SPEC_ITEM , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(SPEC_ITEM , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(SPEC_ITEM , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(SPEC_ITEM , DESCRIPTION, RstPattern.DESCRIPTION, this::beginDescription ),
+ transition(SPEC_ITEM , DESCRIPTION, RstPattern.NOT_EMPTY , this::beginDescription ),
+
+ transition(DESCRIPTION, SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(DESCRIPTION, TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(DESCRIPTION, RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(DESCRIPTION, COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(DESCRIPTION, COVERS , RstPattern.COVERS , () -> {} ),
+ transition(DESCRIPTION, DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(DESCRIPTION, NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(DESCRIPTION, NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(DESCRIPTION, TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(DESCRIPTION, TAGS , RstPattern.TAGS , () -> {} ),
+ transition(DESCRIPTION, START , RstPattern.FORWARD , () -> {endItem(); forward();} ),
+ transition(DESCRIPTION, TITLE , SECTION_TITLE , this::rememberTitle ),
+ transition(DESCRIPTION, DESCRIPTION, RstPattern.EVERYTHING , this::appendDescription ),
+
+ transition(RATIONALE , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(RATIONALE , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(RATIONALE , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(RATIONALE , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(RATIONALE , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(RATIONALE , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(RATIONALE , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(RATIONALE , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(RATIONALE , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(RATIONALE , RATIONALE , RstPattern.EVERYTHING , this::appendRationale ),
+
+ transition(COMMENT , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(COMMENT , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(COMMENT , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(COMMENT , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(COMMENT , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(COMMENT , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(COMMENT , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(COMMENT , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(COMMENT , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(COMMENT , COMMENT , RstPattern.EVERYTHING , this::appendComment ),
+
+ // [impl->dsn~md.covers-list~1]
+ transition(COVERS , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(COVERS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(COVERS , COVERS , RstPattern.COVERS_REF , this::addCoverage ),
+ transition(COVERS , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(COVERS , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(COVERS , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(COVERS , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(COVERS , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(COVERS , COVERS , RstPattern.EMPTY , () -> {} ),
+ transition(COVERS , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(COVERS , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(COVERS , START , RstPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ // [impl->dsn~md.depends-list~1]
+ transition(DEPENDS , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(DEPENDS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(DEPENDS , DEPENDS , RstPattern.DEPENDS_REF, this::addDependency ),
+ transition(DEPENDS , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(DEPENDS , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(DEPENDS , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(DEPENDS , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(DEPENDS , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(DEPENDS , DEPENDS , RstPattern.EMPTY , () -> {} ),
+ transition(DEPENDS , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(DEPENDS , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(DEPENDS , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(DEPENDS , START , RstPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ // [impl->dsn~md.needs-coverage-list-single-line~2]
+ // [impl->dsn~md.needs-coverage-list~1]
+ transition(NEEDS , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(NEEDS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(NEEDS , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(NEEDS , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(NEEDS , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(NEEDS , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(NEEDS , NEEDS , RstPattern.NEEDS_REF , this::addNeeds ),
+ transition(NEEDS , NEEDS , RstPattern.EMPTY , () -> {} ),
+ transition(NEEDS , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(NEEDS , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(NEEDS , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(NEEDS , START , RstPattern.FORWARD , () -> {endItem(); forward();} ),
+
+ transition(TAGS , TAGS , RstPattern.TAG_ENTRY , this::addTag ),
+ transition(TAGS , TITLE , SECTION_TITLE , () -> {endItem(); rememberTitle();}),
+ transition(TAGS , SPEC_ITEM , RstPattern.ID , this::beginItem ),
+ transition(TAGS , RATIONALE , RstPattern.RATIONALE , this::beginRationale ),
+ transition(TAGS , COMMENT , RstPattern.COMMENT , this::beginComment ),
+ transition(TAGS , DEPENDS , RstPattern.DEPENDS , () -> {} ),
+ transition(TAGS , NEEDS , RstPattern.NEEDS_INT , this::addNeeds ),
+ transition(TAGS , NEEDS , RstPattern.NEEDS , () -> {} ),
+ transition(TAGS , NEEDS , RstPattern.EMPTY , () -> {} ),
+ transition(TAGS , COVERS , RstPattern.COVERS , () -> {} ),
+ transition(TAGS , TAGS , RstPattern.TAGS , () -> {} ),
+ transition(TAGS , TAGS , RstPattern.TAGS_INT , this::addTag ),
+ transition(TAGS , START , RstPattern.FORWARD , () -> {endItem(); forward();} )
+ };
+ // @formatter:on
+ }
+
+ private static Transition transition(final LineParserState from, final LineParserState to,
+ final RstPattern pattern, final TransitionAction action)
+ {
+ return new Transition(from, to, pattern.getPattern(), action);
+ }
+}
diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java
new file mode 100644
index 000000000..cd9b77e21
--- /dev/null
+++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RestructuredTextImporterFactory.java
@@ -0,0 +1,22 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import org.itsallcode.openfasttrace.api.importer.*;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+
+/**
+ * {@link ImporterFactory} for reStructuredText files
+ */
+public class RestructuredTextImporterFactory extends RegexMatchingImporterFactory
+{
+ /** Creates a new instance. */
+ public RestructuredTextImporterFactory()
+ {
+ super("(?i).*\\.rst");
+ }
+
+ @Override
+ public Importer createImporter(final InputFile fileName, final ImportEventListener listener)
+ {
+ return new RestructuredTextImporter(fileName, listener);
+ }
+}
diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java
new file mode 100644
index 000000000..3f855f3d9
--- /dev/null
+++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstPattern.java
@@ -0,0 +1,91 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.ForwardingSpecificationItem;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LinePattern;
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.SimpleLinePattern;
+
+/**
+ * Patterns that describe tokens to be recognized within reStructured Text
+ * specifications.
+ */
+enum RstPattern
+{
+ // [impl->dsn~md.specification-item-title~1]
+ // [impl->dsn~md.artifact-forwarding-notation~1]
+
+ // @formatter:off
+ COMMENT("Comment:\\s*"),
+ COVERS("Covers:\\s*"),
+ COVERS_REF(PatternConstants.REFERENCE_AFTER_BULLET),
+ DEPENDS("Depends:\\s*"),
+ DEPENDS_REF(PatternConstants.REFERENCE_AFTER_BULLET),
+ DESCRIPTION("Description:\\s*"),
+ EMPTY("(\\s*)"),
+ EVERYTHING("(.*)"),
+ FORWARD(".*?("
+ + PatternConstants.ARTIFACT_TYPE
+ + "\\s*"
+ + ForwardingSpecificationItem.FORWARD_MARKER
+ + "\\s*"
+ + PatternConstants.ARTIFACT_TYPE
+ + "(?:,\\s*"
+ + PatternConstants.ARTIFACT_TYPE
+ + ")*"
+ + "\\s*"
+ + ForwardingSpecificationItem.ORIGINAL_MARKER
+ + "\\s*"
+ + SpecificationItemId.ID_PATTERN
+ + ").*?"),
+ ID("`?(" + SpecificationItemId.ID_PATTERN + ")`?.*"),
+ NEEDS_INT("Needs:(\\s*\\w+\\s*(?:,\\s*\\w+\\s*)*)"),
+ NEEDS("Needs:\\s*"),
+ NEEDS_REF(PatternConstants.UP_TO_3_WHITESPACES + PatternConstants.BULLETS
+ + "(?:.*\\W)?" //
+ + "(\\p{Alpha}+)" //
+ + "(?:\\W.*)?"),
+ NOT_EMPTY("([^\n\r]+)"),
+ RATIONALE("Rationale:\\s*"),
+ STATUS("Status:\\s*(approved|proposed|draft)\\s*"),
+ TAGS_INT("Tags:(\\s*\\w+\\s*(?:,\\s*\\w+\\s*)*)"),
+ TAGS("Tags:\\s*"),
+ TAG_ENTRY(PatternConstants.UP_TO_3_WHITESPACES + PatternConstants.BULLETS
+ + "\\s*" //
+ + "(.*)"),
+ UNDERLINE("([-=`:.'\"~^_*+#<>]{3,})\\s*");
+ // @formatter:on
+
+ private final LinePattern pattern;
+
+ RstPattern(final String regularExpression)
+ {
+ this.pattern = SimpleLinePattern.of(regularExpression);
+ }
+
+ /**
+ * Get the regular expression pattern object
+ *
+ * @return the pattern
+ */
+ public LinePattern getPattern()
+ {
+ return this.pattern;
+ }
+
+ private static final class PatternConstants
+ {
+ public static final String ARTIFACT_TYPE = "[a-zA-Z]+";
+ public static final String BULLETS = "[+*-]";
+ private static final String UP_TO_3_WHITESPACES = "\\s{0,3}";
+ // [impl->dsn~md.requirement-references~1]
+ public static final String REFERENCE_AFTER_BULLET = UP_TO_3_WHITESPACES
+ + PatternConstants.BULLETS + "(?:.*\\W)?" //
+ + "(" + SpecificationItemId.ID_PATTERN + ")" //
+ + "(?:\\W.*)?";
+
+ private PatternConstants()
+ {
+ // not instantiable
+ }
+ }
+}
diff --git a/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePattern.java b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePattern.java
new file mode 100644
index 000000000..aa866d663
--- /dev/null
+++ b/importer/restructuredtext/src/main/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePattern.java
@@ -0,0 +1,21 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.itsallcode.openfasttrace.importer.lightweightmarkup.statemachine.LinePattern;
+
+class RstSectionTitlePattern implements LinePattern
+{
+ private static final LinePattern UNDERLINE = RstPattern.UNDERLINE.getPattern();
+
+ @Override
+ public Optional> getMatches(final String line, final String nextLine)
+ {
+ if (line != null && nextLine != null && UNDERLINE.getMatches(nextLine, null).isPresent())
+ {
+ return Optional.of(List.of(line));
+ }
+ return Optional.empty();
+ }
+}
diff --git a/importer/restructuredtext/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory b/importer/restructuredtext/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory
new file mode 100644
index 000000000..2f1296348
--- /dev/null
+++ b/importer/restructuredtext/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.importer.ImporterFactory
@@ -0,0 +1 @@
+org.itsallcode.openfasttrace.importer.restructuredtext.RestructuredTextImporterFactory
diff --git a/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePatternTest.java b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePatternTest.java
new file mode 100644
index 000000000..82e04e7f6
--- /dev/null
+++ b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/RstSectionTitlePatternTest.java
@@ -0,0 +1,93 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class RstSectionTitlePatternTest
+{
+
+ static Stream testCases()
+ {
+ return Stream.of(
+ titleNotRecongnized(null, null),
+ titleNotRecongnized(null, "ignored"),
+ titleNotRecongnized(null, "===="),
+ titleNotRecongnized("ignored", null),
+ titleNotRecongnized("# Title", null),
+ titleNotRecongnized("## Title", null),
+ titleNotRecongnized("# Title", "ignored"),
+ testCase("# Title", "=======", "# Title"),
+ testCase("Title with words", "=======", "Title with words"),
+ testCase("\t Leading & trailing whitespace not removed ", "=======",
+ "\t Leading & trailing whitespace not removed "),
+ underlineRecognized("==="),
+ underlineRecognized("---"),
+ underlineRecognized("___"),
+ underlineRecognized(":::"),
+ underlineRecognized("..."),
+ underlineRecognized("^^^"),
+ underlineRecognized("```"),
+ underlineRecognized("\"\"\""),
+ underlineRecognized("'''"),
+ underlineRecognized("^^^"),
+ underlineRecognized("~~~"),
+ underlineRecognized("***"),
+ underlineRecognized("+++"),
+ underlineRecognized("###"),
+ underlineRecognized("<<<"),
+ underlineRecognized(">>>"),
+ underlineRecognized("======="),
+ underlineNotRecognized("=="),
+ underlineNotRecognized("--"),
+ underlineNotRecognized("__"),
+ underlineNotRecognized("^^"));
+ }
+
+ private static Arguments underlineRecognized(final String underline)
+ {
+ return Arguments.of("Title", underline, "Title");
+ }
+
+ private static Arguments underlineNotRecognized(final String underline)
+ {
+ return Arguments.of("Title", underline, null);
+ }
+
+ private static Arguments titleNotRecongnized(final String line, final String nextLine)
+ {
+ return testCase(line, nextLine, null);
+ }
+
+ private static Arguments testCase(final String line, final String nextLine, final String expected)
+ {
+ return Arguments.of(line, nextLine, expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("testCases")
+ void test(final String line, final String nextLine, final String expected)
+ {
+ final RstSectionTitlePattern pattern = new RstSectionTitlePattern();
+ final Optional> result = pattern.getMatches(line, nextLine);
+ if (expected == null)
+ {
+ assertThat("Lines '" + line + "' + '" + nextLine + "' should not be recognized as a section title",
+ result.isPresent(), is(false));
+ }
+ else
+ {
+ assertAll(() -> assertThat(
+ "Lines '" + line + "' + '" + nextLine + "' should be recognized as a section title",
+ result.isPresent(), is(true)), () -> assertThat(result.get().get(0), is(expected)));
+ }
+ }
+}
diff --git a/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java
new file mode 100644
index 000000000..5a733a769
--- /dev/null
+++ b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporter.java
@@ -0,0 +1,146 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import static org.itsallcode.matcher.auto.AutoMatcher.contains;
+import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item;
+
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+import org.itsallcode.openfasttrace.api.importer.ImporterFactory;
+import org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup.AbstractLightWeightMarkupImporterTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestRestructuredTextImporter extends AbstractLightWeightMarkupImporterTest
+{
+ private static final ImporterFactory importerFactory = new RestructuredTextImporterFactory();
+
+ TestRestructuredTextImporter()
+ {
+ super(1);
+ }
+
+ @Override
+ protected ImporterFactory getImporterFactory()
+ {
+ return importerFactory;
+ }
+
+ protected String formatTitle(final String title, final int level)
+ {
+ return title + "\n" + "=".repeat(title.length());
+ }
+
+ // [utest -> dsn~md.specification-item-title~1]
+ @Test
+ void testMarkdownTitleBeforeRequirementIdIsRequirementTitle()
+ {
+ assertImport("titles.md",
+ """
+ The title
+ =========
+ the~id~1
+ """,
+ contains(item()
+ .id("the", "id", 1)
+ .title("The title")
+ .location("titles.md", 3)
+ .build()));
+ }
+
+ // [utest -> dsn~md.specification-item-title~1]
+ @Test
+ void testMarkdownTitleDetectedAfterAnotherTitle()
+ {
+ assertImport("more_titles.md",
+ """
+ 1st level title
+ ===============
+
+ 2nd level title
+ ---------------
+
+ the~id~1
+ """,
+ contains(item()
+ .title("2nd level title")
+ .id("the", "id", 1)
+ .location("more_titles.md", 7)
+ .build()));
+ }
+
+ // [utest->dsn~md.specification-item-title~1]
+ @Test
+ void testFindTitleAfterTitle()
+ {
+ assertImport("x", """
+ This title should be ignored
+ ============================
+
+ Title
+ -----
+ `a~b~1
+ """,
+ contains(item()
+ .id(SpecificationItemId.parseId("a~b~1"))
+ .title("Title").location("x", 6)
+ .build()));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings =
+ { "---------------------------------", "---", "===", "======", "--- ",
+ "=== ", "---\t" })
+ void testRecognizeItemTitleWithUnderlines(final String underline)
+ {
+ assertImport("file name", """
+ This is a title with an underline
+ %s
+ `extra~support-underlined-headers~1`
+ Body text.
+ """.formatted(underline),
+ contains(item()
+ .id(SpecificationItemId.createId("extra", "support-underlined-headers",
+ 1))
+ .title("This is a title with an underline")
+ .description("Body text.")
+ .location("file name", 3)
+ .build()));
+ }
+
+ @ValueSource(strings = { "---------------------------------", "---", "===", "======",
+ "================================================",
+ "--- ", "=== ", "---\t"
+ })
+ @ParameterizedTest
+ void testRecognizeItemTitleWithUnderlinesAfterAnotherTitle(final String underline)
+ {
+ assertImport("y", """
+ # This must be ignored.
+ This is a title with an underline
+ %s
+ `extra~support-underlined-headers~1`
+ Body text.
+ """.formatted(underline),
+ contains(item()
+ .id(SpecificationItemId.createId("extra", "support-underlined-headers",
+ 1))
+ .title("This is a title with an underline")
+ .description("Body text.")
+ .location("y", 4)
+ .build()));
+ }
+
+ @Test
+ void testLessThenThreeUnderliningCharactersAreNotDetectedAsTitleUnderlines()
+ {
+ assertImport("z", """
+ This is not a title since the underline is too short
+ --
+ req~too-short~111
+ """,
+ contains(item()
+ .id(SpecificationItemId.createId("req", "too-short", 111))
+ .location("z", 3)
+ .build()));
+ }
+}
diff --git a/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporterFactory.java b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporterFactory.java
new file mode 100644
index 000000000..d42528ae0
--- /dev/null
+++ b/importer/restructuredtext/src/test/java/org/itsallcode/openfasttrace/importer/restructuredtext/TestRestructuredTextImporterFactory.java
@@ -0,0 +1,31 @@
+package org.itsallcode.openfasttrace.importer.restructuredtext;
+
+import org.itsallcode.openfasttrace.testutil.importer.ImporterFactoryTestBase;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Tests for {@link RestructuredTextImporterFactory}
+ */
+class TestRestructuredTextImporterFactory extends ImporterFactoryTestBase
+{
+ @Override
+ protected RestructuredTextImporterFactory createFactory()
+ {
+ return new RestructuredTextImporterFactory();
+ }
+
+ @Override
+ protected List getSupportedFilenames()
+ {
+ return asList("file.rst", "file.RST", "FILE.rst", "FILE.RST");
+ }
+
+ @Override
+ protected List getUnsupportedFilenames()
+ {
+ return asList("file.rs", "file.rest", "filerst", "file.rst.");
+ }
+}
diff --git a/importer/restructuredtext/src/test/resources/logging.properties b/importer/restructuredtext/src/test/resources/logging.properties
new file mode 100644
index 000000000..b2c9cbc7e
--- /dev/null
+++ b/importer/restructuredtext/src/test/resources/logging.properties
@@ -0,0 +1,11 @@
+handlers = java.util.logging.ConsoleHandler org.itsallcode.openfasttrace.testutil.log.NoOpLoggingHandler
+.level = INFO
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = org.itsallcode.openfasttrace.testutil.log.ShortClassNameFormatter
+java.util.logging.ConsoleHandler.encoding = UTF-8
+
+org.itsallcode.openfasttrace.testutil.log.NoOpLoggingHandler.level = ALL
+
+# Set this to FINEST for debugging the state machine
+org.itsallcode.openfasttrace.testutil.level = FINE
+org.itsallcode.openfasttrace.importer.level = FINE
diff --git a/importer/specobject/.settings/org.eclipse.jdt.core.prefs b/importer/specobject/.settings/org.eclipse.jdt.core.prefs
index 04b36440c..a40522564 100644
--- a/importer/specobject/.settings/org.eclipse.jdt.core.prefs
+++ b/importer/specobject/.settings/org.eclipse.jdt.core.prefs
@@ -1,6 +1,6 @@
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
@@ -20,7 +20,7 @@ org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/importer/tag/.settings/org.eclipse.jdt.core.prefs b/importer/tag/.settings/org.eclipse.jdt.core.prefs
index 04b36440c..a40522564 100644
--- a/importer/tag/.settings/org.eclipse.jdt.core.prefs
+++ b/importer/tag/.settings/org.eclipse.jdt.core.prefs
@@ -1,6 +1,6 @@
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
@@ -20,7 +20,7 @@ org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java
index 7fddf3af2..0454ffc86 100644
--- a/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java
+++ b/importer/tag/src/test/java/org/itsallcode/openfasttrace/importer/tag/TestTagImporter.java
@@ -208,7 +208,7 @@ private List runImporter(final String content)
* Create a test case using the content as input for the
* {@link TagImporter}. Make sure to concatenate the content to avoid
* breaking self-tracing.
- *
+ *
* @param content
* content to parse
* @param itemBuilder
diff --git a/importer/xmlparser/.settings/org.eclipse.jdt.core.prefs b/importer/xmlparser/.settings/org.eclipse.jdt.core.prefs
index 6d1f31917..e2b1419eb 100644
--- a/importer/xmlparser/.settings/org.eclipse.jdt.core.prefs
+++ b/importer/xmlparser/.settings/org.eclipse.jdt.core.prefs
@@ -10,9 +10,9 @@ org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -129,7 +129,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/importer/zip/.settings/org.eclipse.jdt.core.prefs b/importer/zip/.settings/org.eclipse.jdt.core.prefs
index 439aefe45..ffe8a6a5c 100644
--- a/importer/zip/.settings/org.eclipse.jdt.core.prefs
+++ b/importer/zip/.settings/org.eclipse.jdt.core.prefs
@@ -1,9 +1,9 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -28,7 +28,7 @@ org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/oft-self-trace.sh b/oft-self-trace.sh
index 77fc91c33..86d3c35d5 100755
--- a/oft-self-trace.sh
+++ b/oft-self-trace.sh
@@ -15,7 +15,9 @@ if $oft_script trace \
--output-file "$report_file" \
--output-format html \
"$base_dir/doc/spec" \
+ "$base_dir/importer/lightweightmarkup/src" \
"$base_dir/importer/markdown/src" \
+ "$base_dir/importer/restructuredtext/src" \
"$base_dir/importer/specobject/src" \
"$base_dir/importer/zip/src" \
"$base_dir/importer/tag/src" \
diff --git a/parent/pom.xml b/parent/pom.xml
index 2b409fdb1..21b187026 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -8,8 +8,8 @@
Free requirement tracking suite
https://github.com/itsallcode/openfasttrace
- 3.8.0
- 11
+ 4.0.0
+ 17
5.11.0-M1
3.2.5
UTF-8
@@ -129,12 +129,24 @@
${revision}
compile
+
+ org.itsallcode.openfasttrace
+ openfasttrace-importer-lightweightmarkup
+ ${revision}
+ compile
+
org.itsallcode.openfasttrace
openfasttrace-importer-markdown
${revision}
compile
+
+ org.itsallcode.openfasttrace
+ openfasttrace-importer-restructuredtext
+ ${revision}
+ compile
+
org.itsallcode.openfasttrace
openfasttrace-importer-specobject
diff --git a/pom.xml b/pom.xml
index 206a73ab2..925234683 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,8 +21,10 @@
product
exporter/common
exporter/specobject
+ importer/lightweightmarkup
importer/markdown
importer/xmlparser
+ importer/restructuredtext
importer/specobject
importer/tag
importer/zip
diff --git a/product/.settings/org.eclipse.jdt.core.prefs b/product/.settings/org.eclipse.jdt.core.prefs
index e2f103ea8..a975639ee 100644
--- a/product/.settings/org.eclipse.jdt.core.prefs
+++ b/product/.settings/org.eclipse.jdt.core.prefs
@@ -10,9 +10,9 @@ org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -127,7 +127,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/product/pom.xml b/product/pom.xml
index 519f449b6..ced143151 100644
--- a/product/pom.xml
+++ b/product/pom.xml
@@ -23,15 +23,15 @@
org.itsallcode.openfasttrace
- openfasttrace-exporter-common
+ openfasttrace-exporter-specobject
org.itsallcode.openfasttrace
- openfasttrace-exporter-specobject
+ openfasttrace-importer-markdown
org.itsallcode.openfasttrace
- openfasttrace-importer-markdown
+ openfasttrace-importer-restructuredtext
org.itsallcode.openfasttrace
@@ -125,4 +125,4 @@
-
+
\ No newline at end of file
diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java
index fffc8ad0b..c831a9bf2 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java
@@ -68,7 +68,7 @@ void testNoArguments(@SysErr final Capturable err)
private void assertExitWithError(final Runnable runnable, final ExitStatus status,
final String message, final Capturable stream) throws MultipleFailuresError
{
- stream.capture();
+ stream.captureMuted();
assertAll( //
() -> assertExitWithStatus(status.getCode(), runnable),
() -> assertThat(stream.getCapturedData(), startsWith(message)) //
@@ -100,7 +100,7 @@ void testConvertWithoutExplicitInputs(@SysOut final Capturable out)
private void assertExitOkWithStdOutStart(final Runnable runnable, final String outputStart,
final Capturable out) throws MultipleFailuresError
{
- out.capture();
+ out.captureMuted();
assertAll(() -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), () -> assertOutputFileExists(false), //
() -> assertThat(out.getCapturedData(), startsWith(outputStart)));
}
@@ -215,7 +215,7 @@ void testTraceWithReportVerbosityQuietToStdOut(@SysOut final Capturable out) thr
TRACE_COMMAND, this.DOC_DIR.toString(), //
REPORT_VERBOSITY_PARAMETER, "QUIET" //
);
- out.capture();
+ out.captureMuted();
assertAll( //
() -> assertExitWithStatus(ExitStatus.OK.getCode(), runnable), //
() -> assertOutputFileExists(false),
diff --git a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java
index 98962c4b4..4925686eb 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java
@@ -14,6 +14,7 @@
import org.itsallcode.openfasttrace.api.importer.ImporterFactory;
import org.itsallcode.openfasttrace.exporter.specobject.SpecobjectExporterFactory;
import org.itsallcode.openfasttrace.importer.markdown.MarkdownImporterFactory;
+import org.itsallcode.openfasttrace.importer.restructuredtext.RestructuredTextImporterFactory;
import org.itsallcode.openfasttrace.importer.specobject.SpecobjectImporterFactory;
import org.itsallcode.openfasttrace.importer.tag.TagImporterFactory;
import org.itsallcode.openfasttrace.importer.zip.ZipFileImporterFactory;
@@ -36,15 +37,15 @@ void testNoServicesRegistered()
assertThat(voidServiceLoader, emptyIterable());
}
- @SuppressWarnings("unchecked")
@Test
void testImporterFactoriesRegistered()
{
final ImporterContext context = new ImporterContext(null);
final List services = getRegisteredServices(ImporterFactory.class,
context);
- assertThat(services, hasSize(4));
- assertThat(services, contains(instanceOf(MarkdownImporterFactory.class), //
+ assertThat(services, hasSize(5));
+ assertThat(services, containsInAnyOrder(instanceOf(MarkdownImporterFactory.class), //
+ instanceOf(RestructuredTextImporterFactory.class), //
instanceOf(SpecobjectImporterFactory.class), //
instanceOf(TagImporterFactory.class), //
instanceOf(ZipFileImporterFactory.class)));
diff --git a/product/src/test/java/org/itsallcode/openfasttrace/report/TestReportService.java b/product/src/test/java/org/itsallcode/openfasttrace/report/TestReportService.java
index 266ad97f0..d91452257 100644
--- a/product/src/test/java/org/itsallcode/openfasttrace/report/TestReportService.java
+++ b/product/src/test/java/org/itsallcode/openfasttrace/report/TestReportService.java
@@ -1,7 +1,6 @@
package org.itsallcode.openfasttrace.report;
import static org.hamcrest.MatcherAssert.assertThat;
-
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -16,9 +15,7 @@
import org.itsallcode.openfasttrace.api.ReportSettings;
import org.itsallcode.openfasttrace.api.core.Trace;
import org.itsallcode.openfasttrace.api.exporter.ExporterException;
-import org.itsallcode.openfasttrace.api.report.ReportException;
-import org.itsallcode.openfasttrace.api.report.ReportVerbosity;
-import org.itsallcode.openfasttrace.api.report.ReporterContext;
+import org.itsallcode.openfasttrace.api.report.*;
import org.itsallcode.openfasttrace.core.report.ReportService;
import org.itsallcode.openfasttrace.core.report.ReporterFactoryLoader;
import org.junit.jupiter.api.Test;
@@ -41,7 +38,7 @@ void testReportPlainText(@SysOut final Capturable out)
.builder() //
.verbosity(ReportVerbosity.MINIMAL) //
.build();
- out.capture();
+ out.captureMuted();
createService(settings).reportTraceToStdOut(this.traceMock, settings.getOutputFormat());
assertThat(out.getCapturedData(), equalTo("not ok\n"));
}
@@ -53,7 +50,7 @@ void testReportHtml(@SysOut final Capturable out)
.builder() //
.outputFormat("html") //
.build();
- out.capture();
+ out.captureMuted();
createService(settings).reportTraceToStdOut(this.traceMock, settings.getOutputFormat());
assertThat(out.getCapturedData(), startsWith(""));
}
@@ -117,4 +114,4 @@ private void makeFileWritable(final Path readOnlyFilePath)
{
readOnlyFilePath.toFile().setWritable(true);
}
-}
\ No newline at end of file
+}
diff --git a/reporter/aspec/.settings/org.eclipse.jdt.core.prefs b/reporter/aspec/.settings/org.eclipse.jdt.core.prefs
index 2de9d8bc1..c5b728dbf 100644
--- a/reporter/aspec/.settings/org.eclipse.jdt.core.prefs
+++ b/reporter/aspec/.settings/org.eclipse.jdt.core.prefs
@@ -8,8 +8,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.APILeak=warning
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
@@ -119,7 +119,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/reporter/html/.settings/org.eclipse.jdt.core.prefs b/reporter/html/.settings/org.eclipse.jdt.core.prefs
index 2de9d8bc1..c5b728dbf 100644
--- a/reporter/html/.settings/org.eclipse.jdt.core.prefs
+++ b/reporter/html/.settings/org.eclipse.jdt.core.prefs
@@ -8,8 +8,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.APILeak=warning
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
@@ -119,7 +119,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/reporter/html/src/main/java/module-info.java b/reporter/html/src/main/java/module-info.java
index c27dad5c4..741ecc78b 100644
--- a/reporter/html/src/main/java/module-info.java
+++ b/reporter/html/src/main/java/module-info.java
@@ -1,5 +1,5 @@
/**
- * This provides an report generator for the HTML format.
+ * This provides a report generator for the HTML format.
*
* @provides org.itsallcode.openfasttrace.api.report.ReporterFactory
*/
diff --git a/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/TestHtmlReport.java b/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/TestHtmlReport.java
index 49f0bb28c..fb6e69fad 100644
--- a/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/TestHtmlReport.java
+++ b/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/TestHtmlReport.java
@@ -18,6 +18,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.itemWithId;
+
@ExtendWith(MockitoExtension.class)
class TestHtmlReport
{
@@ -48,13 +50,11 @@ protected String renderToString()
void testRenderSimpleTrace()
{
final LinkedSpecificationItem itemA = new LinkedSpecificationItem(
- SpecificationItem.builder() //
- .id(SpecificationItemId.createId("a", "a-item", 1)) //
+ itemWithId(SpecificationItemId.createId("a", "a-item", 1)) //
.description("Description A") //
.build());
final LinkedSpecificationItem itemB = new LinkedSpecificationItem(
- SpecificationItem.builder() //
- .id(SpecificationItemId.createId("b", "b-item", 1)) //
+ itemWithId(SpecificationItemId.createId("b", "b-item", 1)) //
.description("Description b") //
.build());
when(this.traceMock.getItems()).thenReturn(Arrays.asList(itemA, itemB));
diff --git a/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/view/html/TestHtmlSpecificationItem.java b/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/view/html/TestHtmlSpecificationItem.java
index 792dba2d3..ab76227ad 100644
--- a/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/view/html/TestHtmlSpecificationItem.java
+++ b/reporter/html/src/test/java/org/itsallcode/openfasttrace/report/html/view/html/TestHtmlSpecificationItem.java
@@ -22,6 +22,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.itemWithId;
+
@ExtendWith(MockitoExtension.class)
class TestHtmlSpecificationItem extends AbstractTestHtmlRenderer
{
@@ -54,8 +56,7 @@ public void prepareEachTest()
@Test
void testRenderMinimalItem()
{
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.title("Item A title") //
.description("Single line description") //
.build();
@@ -80,8 +81,7 @@ void testRenderDetailsDefaultValue(final DetailsSectionDisplay displayStatus, fi
{
this.factory = HtmlViewFactory.create(this.outputStream, HtmlReport.getCssUrl(),
displayStatus);
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.title("Item A title") //
.description("Single line description") //
.build();
@@ -92,8 +92,7 @@ void testRenderDetailsDefaultValue(final DetailsSectionDisplay displayStatus, fi
@Test
void testRenderMultiLineItem()
{
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_B_ID) //
+ final SpecificationItem item = itemWithId(ITEM_B_ID) //
.title("Item B title") //
.description("Description A\n\nDescription B") //
.rationale("Rationale A\n\nRationale B") //
@@ -127,8 +126,7 @@ protected void renderItemOnIndentationLevel(final SpecificationItem item,
@Test
void testRenderNeeds()
{
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.addNeedsArtifactType(IMPL) //
.addNeedsArtifactType(ITEST) //
.addNeedsArtifactType(UTEST) //
@@ -151,8 +149,7 @@ void testRenderNeeds()
@Test
void testRenderIncomingLinks()
{
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.build();
final LinkedSpecificationItem linkedItem = new LinkedSpecificationItem(item);
linkedItem.addLinkToItemWithStatus(this.itemMockB, LinkStatus.COVERED_SHALLOW);
@@ -181,8 +178,7 @@ void testRenderIncomingLinks()
@Test
void testRenderOutgoingLinks()
{
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.build();
final LinkedSpecificationItem linkedItem = new LinkedSpecificationItem(item);
linkedItem.addLinkToItemWithStatus(this.itemMockB, LinkStatus.COVERS);
@@ -212,13 +208,12 @@ void testRenderOutgoingLinks()
void testRenderOrigin()
{
final Location location = Location.create("foo/bar", 13);
- final SpecificationItem item = SpecificationItem.builder() //
- .id(ITEM_A_ID) //
+ final SpecificationItem item = itemWithId(ITEM_A_ID) //
.location(location) //
.build();
final LinkedSpecificationItem linkedItem = new LinkedSpecificationItem(item);
- final SpecificationItem subItem = SpecificationItem.builder() //
- .id(ITEM_B_ID).location(Location.create("http://example.org/foo.txt", 3)).build();
+ final SpecificationItem subItem = itemWithId(ITEM_B_ID)
+ .location(Location.create("http://example.org/foo.txt", 3)).build();
final LinkedSpecificationItem linkedSubItem = new LinkedSpecificationItem(subItem);
linkedItem.addLinkToItemWithStatus(linkedSubItem, LinkStatus.COVERED_SHALLOW);
final Viewable view = this.factory.createSpecificationItem(linkedItem);
@@ -276,7 +271,7 @@ void testRenderEscapesHtmlElementsInRationale()
private SpecificationItem.Builder defaultItem()
{
- return SpecificationItem.builder().id(ITEM_A_ID);
+ return itemWithId(ITEM_A_ID);
}
private void assertRender(final SpecificationItem.Builder itemBuilder, final Matcher matcher)
diff --git a/reporter/plaintext/.settings/org.eclipse.jdt.core.prefs b/reporter/plaintext/.settings/org.eclipse.jdt.core.prefs
index 04b36440c..a40522564 100644
--- a/reporter/plaintext/.settings/org.eclipse.jdt.core.prefs
+++ b/reporter/plaintext/.settings/org.eclipse.jdt.core.prefs
@@ -1,6 +1,6 @@
eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
@@ -20,7 +20,7 @@ org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/testutil/.settings/org.eclipse.jdt.core.prefs b/testutil/.settings/org.eclipse.jdt.core.prefs
index 752ef0855..5bb3d14ee 100644
--- a/testutil/.settings/org.eclipse.jdt.core.prefs
+++ b/testutil/.settings/org.eclipse.jdt.core.prefs
@@ -8,8 +8,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
-org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
+org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
org.eclipse.jdt.core.compiler.problem.APILeak=warning
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
@@ -119,7 +119,7 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
-org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
diff --git a/testutil/pom.xml b/testutil/pom.xml
index 4be5836cd..b98572f38 100644
--- a/testutil/pom.xml
+++ b/testutil/pom.xml
@@ -25,6 +25,11 @@
junit-jupiter-api
compile
+
+ org.junit.jupiter
+ junit-jupiter-params
+ compile
+
org.hamcrest
hamcrest
@@ -53,6 +58,8 @@
-Xlint:-exports
-Xlint:-requires-automatic
+
+ -Xlint:-requires-transitive-automatic
-Werror
@@ -61,6 +68,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
+
true
diff --git a/testutil/src/main/java/module-info.java b/testutil/src/main/java/module-info.java
index f191b7f54..ab554b94b 100644
--- a/testutil/src/main/java/module-info.java
+++ b/testutil/src/main/java/module-info.java
@@ -7,14 +7,17 @@
exports org.itsallcode.openfasttrace.testutil.cli;
exports org.itsallcode.openfasttrace.testutil.core;
exports org.itsallcode.openfasttrace.testutil.importer;
+ exports org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup;
exports org.itsallcode.openfasttrace.testutil.log;
exports org.itsallcode.openfasttrace.testutil.matcher;
exports org.itsallcode.openfasttrace.testutil.xml;
requires org.hamcrest;
+ requires transitive org.itsallcode.automatcher;
requires transitive org.junit.jupiter.api;
- requires org.mockito;
- requires org.mockito.junit.jupiter;
+ requires transitive org.junit.jupiter.params;
+ requires transitive org.mockito;
+ requires transitive org.mockito.junit.jupiter;
requires java.logging;
requires transitive java.xml;
requires org.itsallcode.openfasttrace.api;
diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java
new file mode 100644
index 000000000..f7055e84c
--- /dev/null
+++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/core/ItemBuilderFactory.java
@@ -0,0 +1,44 @@
+package org.itsallcode.openfasttrace.testutil.core;
+
+import org.itsallcode.openfasttrace.api.core.SpecificationItem;
+import org.itsallcode.openfasttrace.api.core.SpecificationItemId;
+
+/**
+ * The {@link ItemBuilderFactory} class provides convenience methods for creating instances of {@link SpecificationItem}.
+ */
+public class ItemBuilderFactory {
+ private ItemBuilderFactory() {
+ // prevent instantiation.
+ }
+
+ /**
+ * Creates a new instance of {@link SpecificationItem.Builder}.
+ *
+ * @return a new instance of {@link SpecificationItem.Builder}
+ */
+ public static final SpecificationItem.Builder item() {
+ return SpecificationItem.builder();
+ }
+
+ /**
+ * Creates a new instance of {@link SpecificationItem.Builder} with the specified ID.
+ *
+ * @param id ID of the specification item
+ *
+ * @return a new instance of {@link SpecificationItem.Builder} with the specified ID
+ */
+ public static final SpecificationItem.Builder itemWithId(SpecificationItemId id) {
+ return SpecificationItem.builder().id(id);
+ }
+
+ /**
+ * Returns a new instance of {@link SpecificationItem.Builder} with the default filename and the specified line.
+ *
+ * @param line line number of the specification item in the file
+ *
+ * @return a new instance of {@link SpecificationItem.Builder} with the default filename and the specified line
+ */
+ public static final SpecificationItem.Builder itemWithDefaultFilenameInLine(final int line) {
+ return SpecificationItem.builder().location("file", line);
+ }
+}
diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/ImportAssertions.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/ImportAssertions.java
new file mode 100644
index 000000000..a8244e751
--- /dev/null
+++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/ImportAssertions.java
@@ -0,0 +1,54 @@
+package org.itsallcode.openfasttrace.testutil.importer;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.BufferedReader;
+import java.io.StringReader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.logging.Logger;
+
+import org.hamcrest.Matcher;
+import org.itsallcode.openfasttrace.api.core.SpecificationItem;
+import org.itsallcode.openfasttrace.api.importer.*;
+import org.itsallcode.openfasttrace.api.importer.input.InputFile;
+import org.itsallcode.openfasttrace.testutil.importer.input.StreamInput;
+
+public final class ImportAssertions
+{
+ private static final Logger LOGGER = Logger.getLogger(ImportAssertions.class.getName());
+
+ private ImportAssertions()
+ {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Assert that the imported input matches the given matcher and filename.
+ *
+ * @param path
+ * expected filename
+ * @param input
+ * content to be imported
+ * @param matcher
+ * matcher that defines expectation for imported data
+ */
+ public static void assertImportWithFactory(final Path path, final String input,
+ final Matcher> matcher,
+ final ImporterFactory importerFactory)
+ {
+ assertThat(runImporterOnText(path, input, importerFactory), matcher);
+ }
+
+ public static List runImporterOnText(final Path path, final String text,
+ final ImporterFactory importerFactory)
+ {
+ LOGGER.finest("Importing text: ***\n" + text + "\n***");
+ final BufferedReader reader = new BufferedReader(new StringReader(text));
+ final InputFile file = StreamInput.forReader(path, reader);
+ final SpecificationListBuilder specItemBuilder = SpecificationListBuilder.create();
+ final Importer importer = importerFactory.createImporter(file, specItemBuilder);
+ importer.runImport();
+ return specItemBuilder.build();
+ }
+}
diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java
new file mode 100644
index 000000000..7d53d70c0
--- /dev/null
+++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/importer/lightweightmarkup/AbstractLightWeightMarkupImporterTest.java
@@ -0,0 +1,532 @@
+package org.itsallcode.openfasttrace.testutil.importer.lightweightmarkup;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.itsallcode.matcher.auto.AutoMatcher.contains;
+import static org.itsallcode.openfasttrace.testutil.core.ItemBuilderFactory.item;
+import static org.itsallcode.openfasttrace.testutil.importer.ImportAssertions.assertImportWithFactory;
+import static org.itsallcode.openfasttrace.testutil.importer.ImportAssertions.runImporterOnText;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.hamcrest.Matcher;
+import org.itsallcode.openfasttrace.api.core.*;
+import org.itsallcode.openfasttrace.api.importer.ImporterFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.*;
+
+public abstract class AbstractLightWeightMarkupImporterTest
+{
+ private static final Path PATH = Path.of("/a/b/c.markdown");
+ private static final String NL = System.lineSeparator();
+ private static final Pattern TITLE_PLACEHOLDER = Pattern.compile("\\$\\{title\\(\"([^\"]+)\", (\\d+)\\)}");
+ private final int titleLocationOffset;
+
+ protected AbstractLightWeightMarkupImporterTest(final int titleLocationOffset)
+ {
+ this.titleLocationOffset = titleLocationOffset;
+ }
+
+ protected abstract String formatTitle(final String title, int level);
+
+ // [utest -> dsn~md.specification-item-id-format~3]
+ @CsvSource({
+ "at~name~67, at, name, 67",
+ "longartifacttype~name.with.dots~123456789, longartifacttype, name.with.dots, 123456789",
+ "a~b1.c1.d~0, a, b1.c1.d, 0"
+ })
+ @ParameterizedTest
+ void testRequirementIdDetected(final String markdownId, final String expectedArtifactType,
+ final String expectedName, final int expectedRevision)
+ {
+ assertImport(PATH, markdownId, contains(item()
+ .id(expectedArtifactType, expectedName, expectedRevision)
+ .location(PATH.toString(), 1)
+ .build()));
+ }
+
+ protected void assertImport(final String path, final String input,
+ final Matcher> matcher)
+ {
+ assertImport(Path.of(path), input, matcher);
+ }
+
+ protected void assertImport(final Path path, final String input,
+ final Matcher> matcher)
+ {
+ assertImportWithFactory(path, processTextInput(input), matcher, getImporterFactory());
+ }
+
+ private String processTextInput(final String input)
+ {
+ return TITLE_PLACEHOLDER.matcher(input)
+ .replaceAll(match -> formatTitle(match.group(1), Integer.parseInt(match.group(2))));
+ }
+
+ protected abstract ImporterFactory getImporterFactory();
+
+ // [utest -> dsn~md.requirement-references~1]
+ @Test
+ void testSpecificationItemReferenceDetected()
+ {
+ assertImport(PATH, """
+ `req~item-a~2`
+
+ Covers:
+ * `feat~item-b~1`
+ """,
+ contains(item()
+ .id("req", "item-a", 2)
+ .addCoveredId("feat", "item-b", 1)
+ .location(PATH.toString(), 1)
+ .build()));
+ }
+
+ // [utest -> dsn~md.covers-list~1]
+ @Test
+ void testSpecificationItemCoversList()
+ {
+ assertImport(PATH, """
+ req~covers-list~4
+ Covers:
+ * `feat~item-a~1`
+ * `feat~item-b~2`
+ * `feat~item-c~3`
+ """,
+ contains(item()
+ .id("req", "covers-list", 4)
+ .addCoveredId("feat", "item-a", 1)
+ .addCoveredId("feat", "item-b", 2)
+ .addCoveredId("feat", "item-c", 3)
+ .location(PATH.toString(), 1)
+ .build()));
+ }
+
+ // [utest -> dsn~md.depends-list~1]
+ @Test
+ void testSpecificationItemDependsList()
+ {
+ assertImport(PATH, """
+ req~depends-list~4
+ Depends:
+ * `feat~item-a~1`
+ * `feat~item-b~2`
+ * `feat~item-c~3`
+ """,
+ contains(item()
+ .id("req", "depends-list", 4)
+ .addDependOnId("feat", "item-a", 1)
+ .addDependOnId("feat", "item-b", 2)
+ .addDependOnId("feat", "item-c", 3)
+ .location(PATH.toString(), 1)
+ .build()));
+ }
+
+ // [utest -> dsn~md.needs-coverage-list-single-line~2]
+ @Test
+ void testSpecificationItemNeedsCoverageListCompact()
+ {
+ final Path path = Path.of("~/git/foo/bar.md");
+ assertImport(path, """
+ req~needs-coverage-list-single-line~4
+ Needs: dsn, uman
+ """,
+ contains(item()
+ .id("req", "needs-coverage-list-single-line", 4)
+ .addNeedsArtifactType("dsn")
+ .addNeedsArtifactType("uman")
+ .location(path.toString(), 1)
+ .build()));
+ }
+
+ static Stream tags()
+ {
+ return Stream.of(
+ Arguments.of("Tags: req , dsn ", List.of("req", "dsn")),
+ Arguments.of("Tags: req ", List.of("req")),
+ Arguments.of("Tags: req,dsn ", List.of("req", "dsn")),
+ Arguments.of("Tags: req ,dsn", List.of("req", "dsn")),
+ Arguments.of("Tags: req,dsn", List.of("req", "dsn")),
+ Arguments.of("Tags: req,\tdsn\n", List.of("req", "dsn")),
+ Arguments.of("Tags:req,dsn", List.of("req", "dsn")),
+ Arguments.of("Tags:\n* req\n* dsn", List.of("req", "dsn")),
+ Arguments.of("Tags:\n * req\n * dsn\n", List.of("req", "dsn")),
+ Arguments.of("Tags:\n* req \n\t* dsn ", List.of("req", "dsn")),
+ Arguments.of("Tags:\n* req\n* dsn", List.of("req", "dsn")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("tags")
+ void testTags(final String mdContent, final List expected)
+ {
+ final List items = runImporterOnText(Path.of("irrelevant-filename"),
+ "`a~b~1`\n" + mdContent,
+ getImporterFactory());
+ assertThat(items.get(0).getTags(), equalTo(expected));
+ }
+
+ // [utest -> dsn~md.needs-coverage-list~1]
+ @Test
+ void testSpecificationItemNeedsCoverageList()
+ {
+ assertImport("needs-list.md", """
+ req~needs-coverage-list~4
+ Needs:
+ * dsn
+ * uman
+ """,
+ contains(item()
+ .id("req", "needs-coverage-list", 4)
+ .addNeedsArtifactType("dsn")
+ .addNeedsArtifactType("uman")
+ .location("needs-list.md", 1)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @CsvSource({
+ "dsn-->impl:req~foobar~1, dsn, impl, req, foobar, 1",
+ " rsn-->sdk:req~foobar~2, rsn, sdk, req, foobar, 2",
+ "dsn-->impl : test~foobar~3 , dsn, impl, test, foobar, 3",
+ " rsn-->sdk : test~foobar~4 , rsn, sdk, test, foobar, 4",
+ "dsn-->impl\t: req~foobar~1, dsn, impl, req, foobar, 1",
+ "rsn-->sdk\t: \treq~foobar~2, rsn, sdk, req, foobar, 2",
+ "dsn -->impl : test~foobar~3\t , dsn, impl, test, foobar, 3",
+ "rsn -->sdk : test~foobar~4\t\t , rsn, sdk, test, foobar, 4"
+ })
+ @ParameterizedTest
+ void testArtifactForwardingNotation(final String input, final String forwardedArtifactType,
+ final String targetArtifactType, final String originalArtifactType,
+ final String originalName, final int originalRevision)
+ {
+ assertImport("xyz", input,
+ contains(item()
+ .id(forwardedArtifactType, originalName, originalRevision)
+ .addCoveredId(originalArtifactType, originalName, originalRevision)
+ .addNeedsArtifactType(targetArtifactType)
+ .forwards(true)
+ .location("xyz", 1)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testForwardingAfterDepends()
+ {
+ assertImport("1.2.md", """
+ dsn~foo~1
+ Depends:
+ * req~foo~1
+ dsn-->impl:req~bar~2
+ """,
+ contains(
+ item()
+ .id("dsn", "foo", 1)
+ .location("1.2.md", 1)
+ .addDependOnId("req", "foo", 1)
+ .build(),
+ item()
+ .id("dsn", "bar", 2)
+ .addCoveredId("req", "bar", 2)
+ .addNeedsArtifactType("impl")
+ .location("1.2.md", 4)
+ .forwards(true)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testForwardingAfterTags()
+ {
+ assertImport("1.2.md", """
+ dsn~foo~1
+ Tags: vanilla, strawberry, mint
+ dsn-->impl:req~bar~2
+ """,
+ contains(
+ item()
+ .id("dsn", "foo", 1)
+ .location("1.2.md", 1)
+ .addTag("vanilla")
+ .addTag("strawberry")
+ .addTag("mint")
+ .build(),
+ item()
+ .id("dsn", "bar", 2)
+ .addCoveredId("req", "bar", 2)
+ .addNeedsArtifactType("impl")
+ .location("1.2.md", 3)
+ .forwards(true)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testMultipleForwardsInARow()
+ {
+ assertImport("fwd.md", """
+ ${title("A Collection of Different Forwards", 1)}
+ * `arch --> dsn : req~foo~1`
+ * arch -->dsn : req~bar~2 with a comment
+ dsn-->impl : req~zoo~3
+ """,
+ contains(
+ item()
+ .id("arch", "foo", 1).addCoveredId("req", "foo", 1)
+ .addNeedsArtifactType("dsn")
+ .forwards(true)
+ .location("fwd.md", 2 + titleLocationOffset)
+ .build(),
+ item()
+ .id("arch", "bar", 2).addCoveredId("req", "bar", 2)
+ .addNeedsArtifactType("dsn")
+ .forwards(true)
+ .location("fwd.md", 3 + titleLocationOffset)
+ .build(),
+ item()
+ .id("dsn", "zoo", 3).addCoveredId("req", "zoo", 3)
+ .addNeedsArtifactType("impl")
+ .forwards(true)
+ .location("fwd.md", 4 + titleLocationOffset)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testArtifactForwardingAfterARegularSpecificationItem()
+ {
+ assertImport("üöä", """
+ art~name~9876
+
+ ${title("Forwards", 1)}
+ a-->b:c~d~5
+ """,
+ contains(
+ item()
+ .id("art", "name", 9876)
+ .location("üöä", 1)
+ .build(),
+ item()
+ .id("a", "d", 5)
+ .addCoveredId("c", "d", 5)
+ .addNeedsArtifactType("b")
+ .location("üöä", 4 + titleLocationOffset)
+ .forwards(true)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testForwardingAfterCovers()
+ {
+ assertImport("1.2.md", """
+ dsn~foo~1
+ Covers:
+ * req~foo~1
+ dsn-->impl:req~bar~2
+ """,
+ contains(
+ item()
+ .id("dsn", "foo", 1)
+ .location("1.2.md", 1)
+ .addCoveredId("req", "foo", 1)
+ .build(),
+ item()
+ .id("dsn", "bar", 2)
+ .addCoveredId("req", "bar", 2)
+ .addNeedsArtifactType("impl")
+ .location("1.2.md", 4)
+ .forwards(true)
+ .build()));
+ }
+
+ // [utest -> dsn~md.artifact-forwarding-notation~1]
+ @Test
+ void testForwardingAfterNeeds()
+ {
+ assertImport("1.2.md", """
+ dsn~foo~1
+ Needs: impl
+ dsn-->impl:req~bar~2
+ """,
+ contains(
+ item()
+ .id("dsn", "foo", 1)
+ .location("1.2.md", 1)
+ .addNeedsArtifactType("impl")
+ .build(),
+ item()
+ .id("dsn", "bar", 2)
+ .addCoveredId("req", "bar", 2)
+ .addNeedsArtifactType("impl")
+ .location("1.2.md", 3)
+ .forwards(true)
+ .build()));
+ }
+
+ @Test
+ void testComplexRequirement()
+ {
+ assertImport("file name", """
+ ${title("Requirement Title", 1)}
+ `type~id~1`
+ Description
+
+ More description
+
+ Rationale:
+ Rationale
+ More rationale
+
+ Covers:
+
+ * impl~foo1~1
+ + [Link to baz2](#impl~baz2~2)
+
+ Depends:
+
+ + configuration~blubb.blah.blah~4711
+ - db~blah.blubb~42
+
+ Comment:
+
+ Comment
+ More comment
+
+ Needs: artA , artB
+ """,
+ contains(item().id(SpecificationItemId.parseId("type~id~1")).title("Requirement Title")
+ .comment("Comment" + NL + "More comment")
+ .description("Description" + NL + NL + "More description")
+ .rationale("Rationale" + NL + "More rationale")
+ .addNeedsArtifactType("artA").addNeedsArtifactType("artB")
+ .addCoveredId(SpecificationItemId.parseId("impl~foo1~1"))
+ .addCoveredId(SpecificationItemId.parseId("impl~baz2~2"))
+ .addDependOnId(SpecificationItemId
+ .parseId("configuration~blubb.blah.blah~4711"))
+ .addDependOnId(SpecificationItemId.parseId("db~blah.blubb~42"))
+ .location("file name", 2 + titleLocationOffset)
+ .build()));
+ }
+
+ @Test
+ void testTwoConsecutiveSpecificationItems()
+ {
+ assertImport("file1.md", """
+ dsn~foo~1
+ First description
+
+ Comment:
+
+ First comment
+
+ dsn~bar~2
+ Second description
+
+ Rationale:
+ Second rationale
+ """,
+ contains(
+ item()
+ .id("dsn", "foo", 1)
+ .description("First description")
+ .comment("First comment")
+ .location("file1.md", 1)
+ .build(),
+ item()
+ .id("dsn", "bar", 2)
+ .description("Second description")
+ .rationale("Second rationale")
+ .location("file1.md", 8)
+ .build()));
+ }
+
+ static Stream needsCoverage()
+ {
+ return Stream.of(
+ Arguments.of("Needs: req , dsn ", List.of("req", "dsn")),
+ Arguments.of("Needs: req ", List.of("req")),
+ Arguments.of("Needs: req,dsn ", List.of("req", "dsn")),
+ Arguments.of("Needs: req ,dsn", List.of("req", "dsn")),
+ Arguments.of("Needs: req,dsn", List.of("req", "dsn")),
+ Arguments.of("Needs: req,\tdsn\n", List.of("req", "dsn")),
+ Arguments.of("Needs:req,dsn", List.of("req", "dsn")),
+ Arguments.of("Needs:\n* req\n* dsn", List.of("req", "dsn")),
+ Arguments.of("Needs:\n * req\n * dsn", List.of("req", "dsn")),
+ Arguments.of("Needs:\n* req \n\t* dsn ", List.of("req", "dsn")),
+ Arguments.of("Needs:\n* req\n* dsn", List.of("req", "dsn")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("needsCoverage")
+ void testNeedsCoverage(final String mdContent, final List expected)
+ {
+ final List items = runImporterOnText(Path.of("irrelevant-filename"),
+ "`a~b~1`\n" + mdContent,
+ getImporterFactory());
+ assertThat(items.get(0).getNeedsArtifactTypes(), equalTo(expected));
+ }
+
+ @Test
+ void testItemStatsRecognized()
+ {
+ assertImport("status.MD", """
+ `arch~status-enum~1`
+ Status: draft
+ """,
+ contains(item()
+ .id("arch", "status-enum", 1)
+ .status(ItemStatus.DRAFT)
+ .location("status.MD", 1)
+ .build()));
+ }
+
+ // [impl -> dsn~md.specification-item-id-format~3] with UTF-8
+ @Test
+ void testItemIdSupportsUTF8Characaters()
+ {
+ assertImport("umlauts",
+ """
+ ${title("Die Implementierung muss den Zustand einzelner Zellen ändern", 3)}
+ `req~zellzustandsänderung~1
+ Ermöglicht die Aktualisierung des Zustands von lebenden und toten Zellen in jeder Generation.
+ Needs: arch
+ """,
+ contains(item()
+ .id(SpecificationItemId.createId("req", "zellzustandsänderung", 1))
+ .title("Die Implementierung muss den Zustand einzelner Zellen ändern")
+ .description("Ermöglicht die Aktualisierung des Zustands von lebenden und toten Zellen"
+ + " in jeder Generation.")
+ .location("umlauts", 2 + titleLocationOffset)
+ .addNeedsArtifactType("arch")
+ .build()));
+ }
+
+ @Test
+ void testHeaderBelongsToNextItem()
+ {
+ assertImport("file", """
+ ${title("Item 1", 1)}
+ `req~item1~1
+ Item 1 description
+
+ ${title("Item 2", 1)}
+ `req~item2~1
+ Item 2 description
+ """,
+ contains(item().id(SpecificationItemId.createId("req", "item1", 1))
+ .title("Item 1")
+ .description("Item 1 description")
+ .location("file", 2 + titleLocationOffset)
+ .build(),
+ item().id(SpecificationItemId.createId("req", "item2", 1))
+ .title("Item 2")
+ .description("Item 2 description")
+ .location("file", 6 + (2 * titleLocationOffset))
+ .build()));
+ }
+}
diff --git a/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/log/ShortClassNameFormatter.java b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/log/ShortClassNameFormatter.java
new file mode 100644
index 000000000..0df3b81d7
--- /dev/null
+++ b/testutil/src/main/java/org/itsallcode/openfasttrace/testutil/log/ShortClassNameFormatter.java
@@ -0,0 +1,85 @@
+package org.itsallcode.openfasttrace.testutil.log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.logging.Formatter;
+import java.util.logging.LogRecord;
+
+/**
+ * This {@code java.util.logging} {@link Formatter} is based on
+ * {@link java.util.logging.SimpleFormatter}. It uses a fixed, single-line
+ * format and shortens the class name of the logger to the last part of the
+ * fully qualified class name.
+ *
+ * Configure the formatter in {@code logging.properties} like this:
+ *
+ *
+ * java.util.logging.ConsoleHandler.formatter = org.itsallcode.openfasttrace.testutil.log.ShortClassNameFormatter
+ *
+ */
+public final class ShortClassNameFormatter extends Formatter
+{
+ private static final String FORMAT = "%1$tF %1$tT [%4$s] %2$s - %5$s %6$s%n";
+
+ public ShortClassNameFormatter()
+ {
+ // Suppress warning "class ... declares no explicit constructors,
+ // thereby exposing a default constructor"
+ }
+
+ @Override
+ public String format(final LogRecord logRecord)
+ {
+ return String.format(FORMAT,
+ getTimestamp(logRecord),
+ getSource(logRecord),
+ logRecord.getLoggerName(),
+ logRecord.getLevel().toString(),
+ formatMessage(logRecord),
+ formatThrowable(logRecord));
+ }
+
+ private static String getSource(final LogRecord logRecord)
+ {
+ if (logRecord.getSourceClassName() != null)
+ {
+ String source = shortenClassName(logRecord.getSourceClassName());
+ if (logRecord.getSourceMethodName() != null)
+ {
+ source += " " + logRecord.getSourceMethodName();
+ }
+ return source;
+ }
+ else
+ {
+ return logRecord.getLoggerName();
+ }
+ }
+
+ private static ZonedDateTime getTimestamp(final LogRecord logRecord)
+ {
+ return ZonedDateTime.ofInstant(logRecord.getInstant(), ZoneId.systemDefault());
+ }
+
+ private static String formatThrowable(final LogRecord logRecord)
+ {
+ if (logRecord.getThrown() == null)
+ {
+ return "";
+ }
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw);
+ pw.println();
+ logRecord.getThrown().printStackTrace(pw);
+ pw.close();
+ return sw.toString();
+ }
+
+ private static String shortenClassName(final String className)
+ {
+ final String[] parts = className.split("\\.");
+ return parts[parts.length - 1];
+ }
+}