Skip to content

Commit

Permalink
GFM compatible alerts (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
thmarx committed Jan 25, 2024
1 parent 480eb2f commit 2f67ab7
Show file tree
Hide file tree
Showing 47 changed files with 493 additions and 321 deletions.
95 changes: 60 additions & 35 deletions README.md
Expand Up @@ -67,67 +67,92 @@ Extensions can be used by adding them to the options.

```java
Options options = new Options();
options.addExtension(new NotificationExtension());
String html = Marked.marked("! This is an info message", options);
// => <div class="notification_info"><p>This is an info message</p></div>
options.addExtension(new GFMAlertExtension());
String html = Marked.marked("> [!NOTE]\n> This is a note!", options);
```

### Notification extension
### GFMAlert extension

The notification extension helps you to add information messages to your markdown content.
Keep in mind, you still need the CSS to style the messages as desired.
Support for github like [alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts).

#### Info message
```text
! This is an info message
For styling, some project-specific CSS is required. SVG icons are embedded but can be replaced by configuration.

#### Usage

```java
Options options = new Options();
// Override default title for note alert
GFMAlertOptions alertOptions = new GFMAlertOptions();
alertOptions.setTitle(GFMAlerts.Alert.WARNING, "Attention!!!");
GFMAlertExtension gfmAlerts = new GFMAlertExtension(alertOptions);
options.addExtension(gfmAlerts);
String html = Marked.marked("> [!NOTE]\n> This is a note!", options);
```

#### Supported alert types

**Note**
```markdown
> [!NOTE]
> Useful information that users should know, even when skimming content.
```
**Note HTML**
```html
<div class="notification_info">
<p>This is an info message</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Useful information that users should know, even when skimming content.</p>
</div>
```

#### Success message
```text
!v This is a success message
**Tip**
```markdown
> [!TIP]
> Helpful advice for doing things better or more easily.
```
**Tip HTML**
```html
<div class="notification_success">
<p>This is a success message</p>
<div class="markdown-alert markdown-alert-tip">
<p class="markdown-alert-title">Tip</p>
<p>Helpful advice for doing things better or more easily.</p>
</div>
```

#### Warning message
```text
!! This is a warning message
**Important**
```markdown
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
```
**Important HTML**
```html
<div class="notification_warning">
<p>This is a warning message</p>
<div class="markdown-alert markdown-alert-important">
<p class="markdown-alert-title">Important</p>
<p>Key information users need to know to achieve their goal.</p>
</div>
```

#### Error message
```text
!x This is an error message
**Warning**
```markdown
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
```
**Warning HTML**
```html
<div class="notification_error">
<p>This is an error message</p>
<div class="markdown-alert markdown-alert-warning">
<p class="markdown-alert-title">Warning</p>
<p>Urgent info that needs immediate user attention to avoid problems.</p>
</div>
```

#### Multiline notifications
Notifications can span multiple lines.

```text
! This is an info message
! That spans over several lines
**Caution**
```markdown
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
```
**Caution HTML**
```html
<div class="notification_info">
<p>This is an info message
That spans over several lines</p>
<div class="markdown-alert markdown-alert-caution">
<p class="markdown-alert-title">Caution</p>
<p>Advises about risks or negative outcomes of certain actions.</p>
</div>
```

Expand Down
24 changes: 12 additions & 12 deletions src/main/java/io/github/gitbucket/markedj/Lexer.java
Expand Up @@ -159,18 +159,6 @@ protected void token(String src, boolean top, boolean bq, LexerContext context){
}
}

// blockquote
{
List<String> cap = rules.get("blockquote").exec(src);
if(!cap.isEmpty()){
src = src.substring(cap.get(0).length());
context.pushToken(new BlockquoteStartToken());
token(cap.get(0).replaceAll("(?m)^ *> ?", ""), top, true, context);
context.pushToken(new BlockquoteEndToken());
continue;
}
}

{
Extension.LexResult result = null;
for (Extension extension : options.getExtensions()) {
Expand All @@ -185,6 +173,18 @@ protected void token(String src, boolean top, boolean bq, LexerContext context){
}
}

// blockquote
{
List<String> cap = rules.get("blockquote").exec(src);
if(!cap.isEmpty()){
src = src.substring(cap.get(0).length());
context.pushToken(new BlockquoteStartToken());
token(cap.get(0).replaceAll("(?m)^ *> ?", ""), top, true, context);
context.pushToken(new BlockquoteEndToken());
continue;
}
}

// list
{
List<String> cap = rules.get("list").exec(src);
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/io/github/gitbucket/markedj/Options.java
Expand Up @@ -19,7 +19,7 @@ public class Options {
"colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
"i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
"sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
"ul", "input", "del", "hr")
"ul", "input", "del", "hr", "svg", "path")
.addAttributes("a", "href", "title")
.addProtocols("a", "href", "http", "https", "mailto", "ftp", "#")
.addAttributes("blockquote", "cite")
Expand All @@ -34,7 +34,10 @@ public class Options {
.addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width")
.addAttributes("ul", "type")
.addAttributes("input", "type", "checked", "name", "value", "disabled")
.addAttributes(":all", "id", "class", "style");
.addAttributes("svg", "class", "viewBox", "version", "width", "height", "aria-hidden")
.addAttributes("path", "d")
.addAttributes(":all", "id", "class", "style")
;

private List<Extension> extensions = new ArrayList<>();

Expand Down
@@ -0,0 +1,39 @@
/*
* Copyright 2024 GitBucket.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.gitbucket.markedj.extension.gfm.alert;

import java.util.Locale;

/**
*
* @author t.marx
*/
public class DefaultGFMAlertRenderer implements GFMAlertRenderer {

@Override
public String render(GFMAlertOptions options, String message, GFMAlerts.Alert alert) {
if (!message.startsWith("<p>")) {
message = String.format("<p>%s</p>", message);
}

return String.format("<div class=\"markdown-alert markdown-alert-%s\"><p class=\"markdown-alert-title\">%s</p>\n%s</div>",
alert.name().toLowerCase(Locale.ENGLISH),
options.getTitle(alert),
message
);
}

}
Expand Up @@ -13,17 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.gitbucket.markedj.extension.notification;
package io.github.gitbucket.markedj.extension.gfm.alert;

import io.github.gitbucket.markedj.token.Token;

/**
*
* @author t.marx
*/
public class NotificationEndToken implements Token {
public class GFMAlertEndToken implements Token {

public static String TYPE = "GFMAlertEndToken";

@Override
public String getType() {
return "NotificationEndToken";
return TYPE;
}
}
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.gitbucket.markedj.extension.notification;
package io.github.gitbucket.markedj.extension.gfm.alert;

import io.github.gitbucket.markedj.Lexer;
import io.github.gitbucket.markedj.Parser;
Expand All @@ -22,51 +22,60 @@
import io.github.gitbucket.markedj.rule.FindFirstRule;
import io.github.gitbucket.markedj.rule.Rule;
import io.github.gitbucket.markedj.token.Token;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
*
* @author t.marx
*/
public class NotificationExtension implements Extension {
public class GFMAlertExtension implements Extension {

public static String BLOCK_NOTIFICATION = "^((?:(!([xv!]?))[^\n]*(?!^!)\n?)+)";
public static String EXPRESSION = "(?s)(?m)\\A^> \\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\](.+?)(^\n|\\Z)";

private Rule notificationRule() {
return new FindFirstRule(BLOCK_NOTIFICATION);
}
private static final Rule RULE = new FindFirstRule(EXPRESSION);

private GFMAlertRenderer renderer = new DefaultGFMAlertRenderer();

private GFMAlertOptions options;

public GFMAlertExtension () {
this(new GFMAlertOptions());
}

public GFMAlertExtension (GFMAlertOptions options) {
this.options = options;
}

public void setRenderer (GFMAlertRenderer renderer) {
this.renderer = renderer;
}

public void setOptions (GFMAlertOptions options) {
this.options = options;
}


@Override
public LexResult lex(String source, final Lexer.LexerContext context, final TokenConsumer consumer) {
List<String> cap = notificationRule().exec(source);
List<String> cap = RULE.exec(source);
boolean tokenFound = false;
if (!cap.isEmpty()) {
// we have detected several contiguous lines of notifications
// ensure that all are of same kind
String allNotificationsLines = cap.get(0);

String content = cap.get(2);

content = content.replaceAll("(?m)^ *> ?", "");

// if other kind of notifications lines are detected
// let's split them so that they are handled separately
String findOtherLinesPattern = "(?m)^(!" + Notifications.exceptGivenNotificationClass(cap.get(3)) + " .*)";
Matcher otherLinesMatcher = Pattern.compile(findOtherLinesPattern).matcher(cap.get(1));

if (otherLinesMatcher.find()) {
String otherLinesSeparated = otherLinesMatcher.replaceAll("\n$1\n");

// change the source to parse
// replace all the notifications lines with separated notifications lines
// and reparse the string
source = otherLinesSeparated + source.substring(allNotificationsLines.length());
} else {
source = source.substring(allNotificationsLines.length());
context.pushToken(new NotificationStartToken(cap.get(3)));
consumer.token(allNotificationsLines.replaceAll("(?m)^" + cap.get(2) + "[ ]?", ""), false, false, context);
context.pushToken(new NotificationEndToken());
}
source = source.substring(allNotificationsLines.length());
context.pushToken(new GFMAlertStartToken(cap.get(1)));
consumer.token(content, false, false, context);
context.pushToken(new GFMAlertEndToken());

tokenFound = true;
}
Expand All @@ -75,24 +84,24 @@ public LexResult lex(String source, final Lexer.LexerContext context, final Toke

@Override
public boolean handlesToken(String token) {
return NotificationStartToken.TYPE.equals(token);
return GFMAlertStartToken.TYPE.equals(token);
}

@Override
public String parse(Parser.ParserContext context, Function<Parser.ParserContext, String> tok) {
NotificationStartToken t = (NotificationStartToken) context.currentToken();
GFMAlertStartToken t = (GFMAlertStartToken) context.currentToken();
StringBuilder body = new StringBuilder();
while (true) {
Token n = context.nextToken();
if (n == null || n.getType().equals("NotificationEndToken")) {
if (n == null || n.getType().equals(GFMAlertEndToken.TYPE)) {
break;
}
body.append(tok.apply(context));
}
return render(body.toString(), t.getNotification());
}

private String render(String info, Notifications.Notification notification) {
return String.format("<div class=\"notification_%s\">\n%s</div>\n", notification.name().toLowerCase(Locale.ENGLISH), info);
private String render(String message, GFMAlerts.Alert alert) {
return renderer.render(options, message, alert);
}
}

0 comments on commit 2f67ab7

Please sign in to comment.