Skip to content

Commit

Permalink
Add markdown support for bold and italic
Browse files Browse the repository at this point in the history
  • Loading branch information
zapek committed Aug 21, 2023
1 parent 4c2edc2 commit a17a2be
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 by David Gerber - https://zapek.com
*
* This file is part of Xeres.
*
* Xeres is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xeres is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xeres. If not, see <http://www.gnu.org/licenses/>.
*/

package io.xeres.ui.support.contentline;

import javafx.scene.Node;
import javafx.scene.text.Text;

import java.util.Set;

public class ContentEmphasis implements Content
{
public enum Style
{
BOLD,
ITALIC
}

private final Text node;

public ContentEmphasis(String text, Set<Style> style)
{
node = new Text(text);
var css = "";
if (style.contains(Style.BOLD))
{
css += "-fx-font-weight: bold;";
}
if (style.contains(Style.ITALIC))
{
css += "-fx-font-style: italic;";
}
if (!css.isEmpty())
{
node.setStyle(css);
}
}

@Override
public Node getNode()
{
return node;
}
}
49 changes: 7 additions & 42 deletions ui/src/main/java/io/xeres/ui/support/contentline/ContentUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

package io.xeres.ui.support.contentline;

import io.xeres.ui.support.util.Range;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class ContentUtils
Expand All @@ -43,14 +44,14 @@ public static void parseInlineUrls(String s, List<Content> contents)
var currentRange = new Range(matcher);

// Text before/between URLs
var betweenRange = currentRange.textRange(previousRange);
var betweenRange = currentRange.outerRange(previousRange);
if (betweenRange.hasRange())
{
contents.add(new ContentText(s.substring(betweenRange.start, betweenRange.end)));
contents.add(new ContentText(s.substring(betweenRange.start(), betweenRange.end())));
}

// URL
contents.add(new ContentUri(s.substring(currentRange.start, currentRange.end)));
contents.add(new ContentUri(s.substring(currentRange.start(), currentRange.end())));

previousRange = currentRange;
}
Expand All @@ -60,47 +61,11 @@ public static void parseInlineUrls(String s, List<Content> contents)
// Text if no URL at all
contents.add(new ContentText(s));
}
else if (previousRange.end < s.length())
else if (previousRange.end() < s.length())
{
// Text after the last URL
contents.add(new ContentText(s.substring(previousRange.end)));
contents.add(new ContentText(s.substring(previousRange.end())));
}
}

private static class Range
{
private final int start;
private final int end;

public Range(Matcher matcher)
{
start = matcher.start(1);
end = matcher.end();
}

public Range(int start, int end)
{
this.start = start;
this.end = end;
}

public boolean hasRange()
{
return end > start;
}

public Range textRange(Range other)
{
if (other.start > start)
{
// other is after us
return new Range(end, other.start);
}
else
{
// other is before us
return new Range(other.end, start);
}
}
}
}
81 changes: 66 additions & 15 deletions ui/src/main/java/io/xeres/ui/support/markdown/Markdown2Flow.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package io.xeres.ui.support.markdown;

import com.vdurmont.emoji.EmojiParser;
import io.xeres.ui.support.contentline.Content;
import io.xeres.ui.support.contentline.ContentHeader;
import io.xeres.ui.support.contentline.ContentUtils;
import io.xeres.ui.support.contentline.*;
import io.xeres.ui.support.util.Range;
import io.xeres.ui.support.util.SmileyUtils;
import javafx.scene.Node;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Scanner;
import java.util.function.Consumer;
import java.util.regex.Pattern;

public class Markdown2Flow
{
private static final Pattern BOLD_PATTERN = Pattern.compile("(\\*\\*[^* ]((?!\\*\\*).)*[^* ]\\*\\*)");
private static final Pattern ITALIC_PATTERN = Pattern.compile("(\\*[^* ]((?!\\*).)*[^* ]\\*)");

private String input;
private final List<Content> content = new ArrayList<>();

Expand Down Expand Up @@ -51,7 +56,11 @@ private void parse()
}
else if (line.contains("*"))
{
processBoldAndItalic(line);
processPattern(BOLD_PATTERN, line,
lineBold -> content.add(new ContentEmphasis(lineBold, EnumSet.of(ContentEmphasis.Style.BOLD))), 2,
lineBold -> processPattern(ITALIC_PATTERN, lineBold,
lineItalic -> content.add(new ContentEmphasis(lineItalic, EnumSet.of(ContentEmphasis.Style.ITALIC))), 1,
lineItalic -> content.add(new ContentText(lineItalic))));
}
else
{
Expand Down Expand Up @@ -79,11 +88,45 @@ private void processHeader(String line)
content.add(new ContentHeader(line.substring(size).trim() + "\n", size));
}

private void processBoldAndItalic(String line)
private void processPattern(Pattern pattern, String line, Consumer<String> match, int matchStrip, Consumer<String> noMatch)
{
// we can have **hello *my* world** and also *hello **my** world*. a space after * or ** makes it fail
var matcher = pattern.matcher(line);
var previousRange = new Range(0, 0);

while (matcher.find())
{
var currentRange = new Range(matcher);

// Before/between matches
var betweenRange = currentRange.outerRange(previousRange);
if (betweenRange.hasRange())
{
noMatch.accept(line.substring(betweenRange.start(), betweenRange.end()));
}

// Match
match.accept(line.substring(currentRange.start() + matchStrip, currentRange.end() - matchStrip));

previousRange = currentRange;
}

if (!previousRange.hasRange())
{
// If no match at all
noMatch.accept(line);
}
else if (previousRange.end() < line.length())
{
// After the last match
noMatch.accept(line.substring(previousRange.end()));
}
}

// XXX: try bold for now
private enum SANITIZE_MODE
{
NORMAL, // keep text as it is
EMPTY_LINES, // remove useless empty lines
CONTINUATION_BREAK // remove line feed to make a continuation break
}

/**
Expand All @@ -95,34 +138,42 @@ static String sanitize(String input)
{
var lines = input.split("\n");
var sb = new StringBuilder();
var skip = 0; // XXX: use an enum or so... 0 = normal, 1 = empty lines, 2 = continuation break
var skip = SANITIZE_MODE.NORMAL;

for (String s : lines)
{
if (s.trim().isEmpty())
{
// One empty line is treated as a paragraph
if (skip != 1)
if (skip != SANITIZE_MODE.EMPTY_LINES)
{
sb.append("\n\n");
skip = 1;
skip = SANITIZE_MODE.EMPTY_LINES;
}
}
else if (s.startsWith("> "))
else if (s.startsWith("> ") || s.startsWith(">>"))
{
// We don't process quoted text
skip = 0;
skip = SANITIZE_MODE.NORMAL;
sb.append(s.stripTrailing()).append("\n");
}
else
{
// Normal break is treated as continuation
if (skip == 2)
if (skip == SANITIZE_MODE.CONTINUATION_BREAK)
{
sb.append(" ");
if (s.stripIndent().startsWith("- ") || s.stripIndent().startsWith("* "))
{
// Except quoted text
sb.append("\n");
}
else
{
sb.append(" ");
}
}
sb.append(s.stripTrailing());
skip = 2;
skip = SANITIZE_MODE.CONTINUATION_BREAK;
}
}
return sb.toString();
Expand Down
73 changes: 73 additions & 0 deletions ui/src/main/java/io/xeres/ui/support/util/Range.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2023 by David Gerber - https://zapek.com
*
* This file is part of Xeres.
*
* Xeres is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xeres is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xeres. If not, see <http://www.gnu.org/licenses/>.
*/

package io.xeres.ui.support.util;

import java.util.regex.Matcher;

/**
* Helper class to handle ranges of text. Provide a matcher then you can find the start and end of the range but
* also the surrounding parts.
*/
public class Range
{
private final int start;
private final int end;

public Range(Matcher matcher)
{
start = matcher.start(1);
end = matcher.end();
}

public Range(int start, int end)
{
this.start = start;
this.end = end;
}

public boolean hasRange()
{
return end > start;
}

public Range outerRange(Range other)
{
if (other.start > start)
{
// other is after us
return new Range(end, other.start);
}
else
{
// other is before us
return new Range(other.end, start);
}
}

public int start()
{
return start;
}

public int end()
{
return end;
}
}

0 comments on commit a17a2be

Please sign in to comment.