Untitled

mail@pastecode.io avatar
unknown
java
a year ago
18 kB
2
Indexable
Never
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// ------------------------------------------------------ //
public abstract class LearningCard {

    protected String title;

    protected String body;

    public LearningCard(String body, String title) {
        this.title = title;
        this.body = body;
    }

    public String getTitle() {
        return this.title;
    }

    /**
     * return the front site of the card
     */
    public abstract List<String> getFrontContent();

    /**
     * return the back site of the card
     */
    public abstract List<String> getBackContent();

    /**
     * return combined content of front and back
     */
    public abstract List<String> getContent();

    /**
     * write the content to the console in a meaningful way
     */
    public abstract void printToConsole();
}

// ------------------------------------------------------ //
class SimpleCard extends LearningCard {

    public SimpleCard(String body, String title) {
        super(body, title);
    }

    @Override
    public List<String> getFrontContent() {
        List<String> c = new ArrayList<>();
        c.add(this.title);

        return c;
    }

    @Override
    public List<String> getBackContent() {
        List<String> c = new ArrayList<>();
        c.add(this.body);

        return c;
    }

    @Override
    public List<String> getContent() {
        List<String> c = new ArrayList<>();
        c.addAll(this.getFrontContent());
        c.addAll(this.getBackContent());

        return c;
    }

    @Override
    public void printToConsole() {
        List<String> c = this.getContent();
        System.out.println("=== " + title + " ===");
        System.out.printf(
                "FRONT: %s\nBACK: %s\n",
                c.get(0),
                c.get(1));
        System.out.println("===");
    }
}

// ------------------------------------------------------ //
class QuestionCard extends LearningCard {

    private String backContent = "";
    private String frontContent = "";


    public QuestionCard(String body, String title) {
        super(body, title);
        parseBody();
    }

    private void parseBody() {
        String[] segments = this.body.split("(?m)^(?=#{2}[^#])");
        for (String segment : segments) {
            Pattern p = Pattern.compile("^#{2}\\s+(?<subname>.*?)?\\s*?(\\{(?<subtag>[^{}]+)\\})?$\\s*(?s)(?<subbody>.*)\\s*", Pattern.MULTILINE);
            Matcher m = p.matcher(segment);

            if (!m.matches()) {
                continue;
            }

            String subBody = Objects.toString(m.group("subbody"), "");
            String subTitle = Objects.toString(m.group("subname"), "");
            String subTag = Objects.toString(m.group("subtag"), "");
            setContent(subTag, "**" + subTitle + "**: \n" + subBody);
        }
    }

    /**
     * Add content to the right spot depending on tag given
     *
     * @param tag
     * @param content
     */
    private void setContent(String tag, String content) {
        switch (tag) {
            case "BACK" -> this.backContent = content;
            case "FRONT" -> this.frontContent = content;
        }
    }

    @Override
    public List<String> getFrontContent() {
        return List.of(this.frontContent);
    }

    @Override
    public List<String> getBackContent() {
        return List.of(backContent);
    }

    @Override
    public List<String> getContent() {
        List<String> c = new ArrayList<>();
        c.addAll(this.getFrontContent());
        c.addAll(this.getBackContent());

        return c;
    }

    @Override
    public void printToConsole() {
        List<String> c = this.getContent();

        System.out.println("=== " + title + " ===");
        System.out.printf(
                "FRONT: %s\nBACK: %s\n",
                c.get(0),
                c.get(1));
        System.out.println("===");
    }
}

class CLOZE extends LearningCard {

    private final String frontContent;
    private final String backContent;

    /**
     * title is empty, body is singular LKME
     *
     * @param body  raw body of learning card
     * @param title title text of the card
     */
    public CLOZE(String body, String title) {
        super(body, title);
        Pattern p = Pattern.compile("(?<=[^^]|^)\\^(?<word>[^^]\\w*[^^])\\^(?=[^^]|$)", Pattern.MULTILINE);
        Matcher m = p.matcher(body);
        List<String> gaps = new ArrayList<>();
        while (m.find()) {
            gaps.add(m.group("word"));
        }
        String gaptext = m.replaceAll("**_____**");
        String fulltext = m.replaceAll("$1");

        //Make ^^ => ^ to allow the restricted use of ^ if needed
        gaptext = gaptext.replaceAll("\\^{2}", "^");
        fulltext = fulltext.replaceAll("\\^{2}", "^");


        this.frontContent = gaptext + "\nAllowed words: " + gaps;
        this.backContent = fulltext;

    }

    @Override
    public List<String> getFrontContent() {
        return List.of(frontContent);
    }

    @Override
    public List<String> getBackContent() {
        return List.of(backContent);
    }

    @Override
    public List<String> getContent() {
        List<String> c = new ArrayList<>();
        c.addAll(getFrontContent());
        c.addAll(getBackContent());

        return c;
    }

    @Override
    public void printToConsole() {
        List<String> c = this.getContent();
        System.out.printf("FRONT: %s\nBACK: %s", c.get(0), c.get(1));
    }
}

enum ConversionGoal {
    HTML
}


record SemiParsedTextFragment<T extends RichText>(String before, T parsed, String after) {
}

interface RichTextParsable {
    public Class<? extends Element> getElementClass();

    public Point getMatchingRange(String s);

    public SemiParsedTextFragment parse(String parsableString);
}

class RichTextParser implements RichTextParsable {

    private final Class<? extends Element> elementClass;

    /**
     * Pattern should have a single capturing group that captures the text that should be considered Element
     */
    private final Pattern matchingPattern;

    public RichTextParser(Class<? extends Element> elementClass, Pattern matchingPattern) {
        this.elementClass = elementClass;
        this.matchingPattern = matchingPattern;
    }


    public Class<? extends Element> getElementClass() {
        return null;
    }

    @Override
    public Point getMatchingRange(String s) {
        return null;
    }

    @Override
    public SemiParsedTextFragment parse(String parseableString) {
        Matcher m = this.matchingPattern.matcher(parseableString);
        if (!m.matches()) return new SemiParsedTextFragment(parseableString, null, "");

        String rawElem = m.group(1);
        int start = m.start();
        int end = m.end();

        return new SemiParsedTextFragment(parseableString.substring(0, start), null, parseableString.substring(end));
    }
}


interface RichText {
    String generate(ConversionGoal c);

    String generateHTML();

}

abstract class ElementParser<T extends Element> {

    public ElementParser(Pattern parsingPattern, List<ElementParser<? extends Element>> allowedChildParsers) {
        this.parsingPattern = parsingPattern;
        this.allowedChildParsers = allowedChildParsers;
    }

    /**
     * Gives pattern that is used to parse markdown
     *
     * @return
     */
    public Pattern getParserPattern() {
        return parsingPattern;
    }

    /**
     * Get position in string that matches
     *
     * @return x=start, y=end
     */
    public Point getMatchRange(String markdown) {
        Matcher m = this.parsingPattern.matcher(markdown);
        if (!m.matches()) return null;

        return new Point(m.start(), m.end());
    }

    /**
     * Parses given markdown and returns result. Everything that wasnt parsed will be given back as strings (before and after) the parsed text
     *
     * @param markdown the text to parse
     * @return the parsing result
     * @see SemiParsedTextFragment
     */
    abstract SemiParsedTextFragment<T> parse(String markdown);

    protected final Pattern parsingPattern;

    protected Children parseChildren(String markdown) {
        SemiParsedSequence s = new SemiParsedSequence(markdown);

        while (!s.fullyParsed()) {
            s.parseNext(this.allowedChildParsers);
        }

        return s.toChildren();

    }

    /**
     * The list of parsers that are allowed to parse the body of a parsed element, allowing recursive parsing (e.g.: Header(Bold(...)))
     */
    protected final List<ElementParser<? extends Element>> allowedChildParsers;

}

class SemiParsedSequence {

    private final List<Object> sequence;

    /**
     * Generate from List **removes every entry that isn't String or Element**
     *
     * @param rawSequence existing list
     */
    public SemiParsedSequence(List<Object> rawSequence) {
        this.sequence = rawSequence.stream().filter(e -> (e instanceof String) || (e instanceof Element)).toList();
    }

    /**
     * Generate from given markdown
     * @param markdown markdown text to generate from
     */
    public SemiParsedSequence(String markdown) {
        this.sequence = List.of(markdown);
    }

    public SemiParsedSequence(SemiParsedSequence sequence) {
        this.sequence = new ArrayList<>(sequence.sequence);
    }

    public boolean fullyParsed() {
        return this.sequence.stream().anyMatch(e -> e instanceof String);
    }

    public void parseNext(final List<ElementParser<? extends Element>> allowedParsers) {
        if (allowedParsers.isEmpty()) return;

        Integer idx = getNextStringPos();
        if (idx == null) return;

        assert sequence.get(idx) instanceof String;
        String markdown = (String) sequence.get(idx);

        // Get the parsers who's match matches first
        Optional<ElementParser<? extends Element>> _parser =
                allowedParsers.stream()
                        .min(Comparator.comparingInt(x -> x.getMatchRange(markdown).x));
        assert _parser.isPresent();

        ElementParser<? extends Element> parser = _parser.get();
        SemiParsedTextFragment<? extends Element> res = parser.parse(markdown);

        insertParsedResult(idx, res);
    }

    private void insertParsedResult(int idx, SemiParsedTextFragment<? extends Element> parsed) {
        // insert parse result in right order into sequence (the element that was at idx will be replaced by the parsed result)
        if (parsed.after() != null && !parsed.after().isEmpty()) sequence.add(idx+1, parsed.after());
        if (parsed.parsed() != null) sequence.set(idx, parsed.parsed());
        if (parsed.before() != null && !parsed.before().isEmpty()) sequence.add(idx, parsed.before());
    }


    private Integer getNextStringPos() {
        for (int i = 0; i < sequence.size(); i++) {
            if (sequence.get(i) instanceof String) return i;
        }
        return null;
    }

    public Children toChildren() {
        if (!this.fullyParsed()) return null;

        return new Children(this.sequence.stream().filter(e -> e instanceof Element).map(e -> (Element)e).toList());
    }

}


abstract class Element implements RichText {
    @Override
    public String generate(ConversionGoal c) {
        switch (c) {
            case HTML -> {
                return this.generateHTML();
            }
            default -> {
                return "";
            }
        }
    }
}


class RawElementParser extends ElementParser<RawElementParser.RawElement> {
    static final private Pattern PARSING_PATTERN = Pattern.compile(".*", Pattern.MULTILINE);
    public RawElementParser() {
        super(PARSING_PATTERN, null);
    }

    @Override
    public SemiParsedTextFragment<RawElement> parse(String markdown) {
        Matcher m = this.parsingPattern.matcher(markdown);
        if (!m.matches()) return new SemiParsedTextFragment<>(markdown, null, null);

        return new SemiParsedTextFragment<>(
                markdown.substring(0, m.start()), // before
                new RawElement(m.group()), // parsed part
                markdown.substring(m.end()) // after
        );
    }

    /**
     * Used to write raw text
     */
    static class RawElement extends Element {


        String element;

        public RawElement(String element) {
            this.element = element;
        }

        @Override
        public String generateHTML() {
            return element;
        }
    }
}


class TextFormattingParser extends ElementParser<TextFormattingParser.TextFormatting> {

    // see: https://regex101.com/r/SXX4pF/1
    static final private Pattern PARSING_PATTERN = Pattern.compile("(?<=^|[^*~])(?<formattype>[*]{1,3}|~{2})(?<body>.*)\\1(?=[^*~]|$)", Pattern.MULTILINE);

    public TextFormattingParser() {
        super(PARSING_PATTERN, List.of(new TextFormattingParser(), new RawElementParser() /*TODO: Add link pasrser*/));
    }

    @Override
    SemiParsedTextFragment<TextFormatting> parse(String markdown) {
        Matcher m = parsingPattern.matcher(markdown);
        if (!m.matches()) return new SemiParsedTextFragment<>(markdown, null, null);
        return new SemiParsedTextFragment<>(
                markdown.substring(0, m.start()),
                new TextFormatting(m.group("formattype"), this.parseChildren(m.group("body"))),
                markdown.substring(m.end()));
    }

    /**
     * HTML
     */
    static class TextFormatting extends Element {

        /**
         * Used by parser
         * @param decoIndicator indicates which decoration to use (*, **, *** or ~~)
         * @param element the child element
         */
        public TextFormatting(String decoIndicator, Element element) {
            this.deco = TextDeco.valueOf(decoIndicator);
            this.element = element;
        }

        enum TextDeco {
            BOLD("**"),
            ITALICS("*"),
            UNDERLINED("~~"),
            BOLDITALIC("***");

            public final String label;

            TextDeco(String label) {
                this.label = label;
            }
        }

        TextDeco deco;

        Element element;

        public String generateHTML() {
            ConversionGoal cGoal = ConversionGoal.HTML;
            switch (deco) {
                case BOLD -> {
                    return "<b>" + element.generate(cGoal) + "</b>";
                }
                case ITALICS -> {
                    return "<i>" + element.generate(cGoal) + "</i>";
                }
                case UNDERLINED -> {
                    return "<u>" + element.generate(cGoal) + "</u>";
                }
                case BOLDITALIC -> {
                    return "<b><i>" + element.generate(cGoal) +"</i></b>";
                }
                default -> {
                    return element.generate(cGoal);
                }
            }
        }
    }
}


class HyperText extends Element {

    String url;
    RichText element;

    @Override
    public String generateHTML() {
        return String.format("<a href=\"%s\">%s</a>", url, element.generate(ConversionGoal.HTML));
    }
}

class Image extends Element {
    File imgFile;

    @Override
    public String generateHTML() {
        BufferedImage img;
        try {
            img = ImageIO.read(imgFile);
        } catch (IOException e) {
            System.err.printf("Unable to read image at: %s\n", imgFile.getAbsolutePath());
            return "";
        }
        ByteArrayOutputStream resImg = new ByteArrayOutputStream();
        try {
            ImageIO.write(img, "png", resImg);
        } catch (IOException e) {
            System.err.printf("Unable to convert image to png: %s", e.getMessage());
        }
        String b64PngImg = Base64.getEncoder().encodeToString(resImg.toByteArray());
        ;
        return "data:image/png;base64," + b64PngImg;
    }
}

class Header extends Element {
    int headerLvl = 1;
    RichText element;

    @Override
    public String generateHTML() {
        int renderedHeaderLvl;
        if (headerLvl < 1) return element.generateHTML();
        else renderedHeaderLvl = Math.min(headerLvl, 6);

        return String.format("<h{0}>" + element.generateHTML() + "</h{0}>", renderedHeaderLvl);
    }
}

class ListContainerElement extends Element {

    enum ListType {
        UNORDERED,
        ORDERED
    }

    ListType t;
    ListElement[] elements;

    @Override
    public String generateHTML() {
        String HTMLListElems = String.join(
                "\n",
                Arrays.stream(elements)
                        .map(ListElement::generateHTML)
                        .toArray(String[]::new));

        if (t == ListType.ORDERED) {
            return "<ol>\n" + HTMLListElems + "\n</ol>";
        }
        return "<ul>\n" + HTMLListElems + "\n</ul>";
    }
}

class ListElement extends Element {

    RichText element;

    @Override
    public String generateHTML() {
        return "<li>" + element.generateHTML() + "</li>";
    }
}

class Code extends Element {
    enum CodeStyle {
        INLINE,
        MULTILINE
    }

    CodeStyle s;
    RawElementParser.RawElement e;


    @Override
    public String generateHTML() {
        if (s == CodeStyle.INLINE) return "<code>" + e.generateHTML() + "</code>";
        return "<pre>" + e.generateHTML() + "</pre>";
    }
}

/**
 * Special type of Element that allow to hold multiple elements
 */
class Children extends Element {

    private List<Element> children;

    public Children(List<Element> children) {
        this.children = new ArrayList<>(children);
    }

    public Children(Element... element) {
       this(Arrays.stream(element).toList());
    }
    @Override
    public String generateHTML() {
        return children.stream().map(RichText::generateHTML).reduce("", String::concat);
    }
}