• Stars
    star
    360
  • Rank 114,045 (Top 3 %)
  • Language
    Kotlin
  • License
    Other
  • Created about 6 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Extensible Android library for both parsing text into Abstract Syntax Trees and rendering those trees as rich text.

SimpleAST

SimpleAST is a Kotlin/Java library designed to parse text into Abstract Syntax Trees. It is heavily inspired by (and began as a port of) Khan Academy's simple-markdown.

It strives for extensibility and robustness. How text is parsed into nodes in a tree is determined by a set of rules, provided by the client. This makes detecting and rendering your own custom entities in text a breeze.

Besides basic markdown, SimpleAST is what the Discord Android app uses to detect and render various entities in text.

For example:

"<@123456789> has **joined the server**." becomes "@AndyG has joined the server." Read more here: How Discord Renders Rich Messages on the Android App

Using SimpleAST in your application

If you are building with Gradle, see https://jitpack.io/#discord/SimpleAST/ for instructions:

Latest versions can be found on the releases page.

Basic Usage with SimpleMarkdownRenderer

If you want to simply render some text with basic markdown, you can use SimpleRenderer:

val source = "here is some bold text: **this is bold**"
val textView = findViewById<TextView>(R.id.textView)

SimpleRenderer.render(source, textView)

SimpleRenderer.render uses the rules provided in SimpleMarkdownRules.kt. These rules currently include:

  • Bold: **bold**
  • Italics 1: *italics*
  • Italics 2: _italics_
  • Underline: __underline__
  • Strikethru: ~~Strikethru~~
  • Escaping: \*Not Italics*

Adding your own Rules

We can create rules which will detect other entities in text. Rules should detect text that begins with symbols or other characters not matched in the plaintext rule.

A few things to keep in mind when building your own Parsers and Rules:

  1. Always include, at the very least, the plaintext rule. Without this rule, you may end up with unmatched text in the source (a fatal error.)
  2. A Pattern that defines a Rule should begin with a symbol that is non-alphanumeric. The plaintext rule is designed so that a non-alphanumeric character will trigger the Parser to consider whether the source matches any other rules first, before consuming it as plaintext.
  3. The Pattern that defines the rule matches only with the beginning of source text (i.e. begins with the '^' character). You will end up with magically-disappearing text if you suddenly match something in the middle of your source.

Simplest example

Let's imagine we want to render all occurrences of <Foo> as Bar, i.e. "This is <Foo> speaking" becomes "This is Bar speaking".

We create a simple Rule that detects and performs the replacement:

class FooRule : Rule<Any?, Node<Any?>>(Pattern.compile("^<Foo>")) {
  override fun parse(matcher: Matcher, parser: Parser<Any?, in Node<Any?>>, isNested: Boolean): ParseSpec<Any?, Node<Any?>{
    return ParseSpec.createTerminal(TextNode("Bar"))
  }
}

Now we create a Parser, add that Rule (and the rest of the basic rules) and render it.

val parser = Parser<Any?, Node<Any?>>()
  .addRule(FooRule())
  .addRules(SimpleMarkdownRules.createSimpleMarkdownRules())
  
resultText.text = SimpleRenderer.render(
    source = input.text,
    parser = parser,
    renderContext = null
)
Input Output
Hello **<Foo>** Hello Bar

Slightly more complex

Suppose we want to replace all occurrences of <1234> (where "1234" is a user id) with UserNode.

We'll create the Rule the same as before.

class UserNode(private val userId: Int) : Node<Any?>() {
  override fun render(builder: SpannableStringBuilder, renderContext: Any?) {
    builder.append("User $userId")
  }
}

class UserMentionRule : Rule<Any?, UserNode>(Pattern.compile("^<(\\d+)>")) {
  override fun parse(matcher: Matcher, parser: Parser<Any?, in UserNode>, isNested: Boolean): ParseSpec<Any?, UserNode> {
    return ParseSpec.createTerminal(UserNode(matcher.group(1).toInt()))
  }
}

The usage is the analogous to the first example.

Input Output
Hello <1234> Hello User 1234

Real-world application: Adding a Render Context

We modify our UserNode thusly to specify that it requires an instance of RenderContext in order to render, which contains a map of Int -> username.

data class RenderContext(val usernameMap: Map<Int, String>)

class UserNode(private val userId: Int) : Node<RenderContext>() {
  override fun render(builder: SpannableStringBuilder, renderContext: RenderContext) {
    builder.append(renderContext.usernameMap[userId] ?: "Invalid User")
  }
}

class UserMentionRule : Rule<RenderContext, UserNode>(Pattern.compile("^<(\\d+)>")) {
  override fun parse(matcher: Matcher, parser: Parser<RenderContext, in UserNode>, isNested: Boolean): ParseSpec<RenderContext, UserNode> {
    return ParseSpec.createTerminal(UserNode(matcher.group(1).toInt()))
  }
}

Now at the call-site, we specify that the parser produces nodes that require the RenderContext, and perform the parse:

val parser = Parser<RenderContext, Node<RenderContext>>()
    .addRule(UserMentionRule())
    .addRules(SimpleMarkdownRules.createSimpleMarkdownRules())

resultText.text = SimpleRenderer.render(
    source = input.text,
    parser = parser,
    renderContext = RenderContext(mapOf(1234 to "CoolDude1234"))
)

Note that we only provide {1234 : "CoolDude1234"} as our map of usernames. As such, we will render the following:

Input Output
Hello <1234> Hello CoolDude1234
Hello <6789> Hello Invalid User

More Repositories

1

discord-api-docs

Official Discord API Documentation
Markdown
5,543
star
2

lilliput

Resize images and animated GIFs in Go
C++
1,923
star
3

manifold

Fast batch message passing between nodes for Erlang/Elixir.
Elixir
1,618
star
4

sorted_set_nif

Elixir SortedSet backed by a Rust-based NIF
Elixir
1,532
star
5

discord-open-source

List of open source communities living on Discord
JavaScript
1,319
star
6

focus-rings

A centralized system for displaying and stylizing focus indicators anywhere on a webpage.
TypeScript
1,120
star
7

fastglobal

Fast no copy globals for Elixir & Erlang.
Elixir
1,097
star
8

discord-rpc

C++
983
star
9

airhornbot

The only bot for Discord you'll ever need.
TypeScript
851
star
10

semaphore

Fast semaphore using ETS.
Elixir
718
star
11

react-dnd-accessible-backend

An add-on backend for `react-dnd` that provides support for keyboards and screenreaders by default.
TypeScript
576
star
12

ex_hash_ring

A fast consistent hash ring implementation in Elixir.
Elixir
475
star
13

discord-example-app

Basic Discord app with examples
JavaScript
434
star
14

OverlappingPanels

Overlapping Panels is a gestures-driven navigation UI library for Android
Kotlin
420
star
15

discord-interactions-js

JS/Node helpers for Discord Interactions
TypeScript
345
star
16

instruments

Simple and Fast metrics for Elixir
Elixir
295
star
17

focus-layers

Tiny React hooks for isolating focus within subsections of the DOM.
TypeScript
292
star
18

discord-api-spec

OpenAPI specification for Discord APIs
237
star
19

discord-oauth2-example

Discord OAuth2 Example
Python
225
star
20

loqui

RPC Transport Layer - with minimal bullshit.
Rust
220
star
21

erlpack

High Performance Erlang Term Format Packer
Cython
211
star
22

cloudflare-sample-app

Example discord bot using Cloudflare Workers
JavaScript
197
star
23

access

Access, a centralized portal for employees to transparently discover, request, and manage their access for all internal systems needed to do their jobs
Python
190
star
24

use-memo-value

Reuse the previous version of a value unless it has changed
TypeScript
170
star
25

zen_monitor

Efficient Process.monitor replacement
Elixir
167
star
26

deque

Fast bounded deque using two rotating lists.
Elixir
141
star
27

avatar-remix-bot

TypeScript
127
star
28

linked-roles-sample

JavaScript
119
star
29

punt

Punt is a tiny and lightweight daemon which helps ship logs to Elasticsearch.
Go
113
star
30

embedded-app-sdk

🚀 The Discord Embedded App SDK lets you build rich, multiplayer experiences as Activities inside Discord.
TypeScript
109
star
31

sample-game-integration

An example using Discord's API and local RPC socket to add Voice and Text chat to an instance or match based multiplayer game.
JavaScript
105
star
32

endanger

Build Dangerfiles with ease.
TypeScript
96
star
33

discord-interactions-python

Useful tools for building interactions in Python
Python
93
star
34

react-base-hooks

Basic utility React hooks
TypeScript
77
star
35

dynamic-pool

a lock-free, thread-safe, dynamically-sized object pool.
Rust
76
star
36

itsdangerous-rs

A rust port of itsdangerous!
Rust
72
star
37

gen_registry

Simple and efficient local Process Registry
Elixir
71
star
38

confetti-cannon

Launch Confetti
TypeScript
45
star
39

discord-react-forms

Forms... in React
JavaScript
43
star
40

discord-interactions-php

PHP utilities for building Discord Interaction webhooks
PHP
40
star
41

babel-plugin-define-patterns

Create constants that replace various expressions at build-time
JavaScript
39
star
42

eslint-traverse

Create a sub-traversal of an AST node in your ESLint plugin
JavaScript
30
star
43

rapidxml

Mirror of rapidxml from http://rapidxml.sourceforge.net/
C++
29
star
44

memory_size

Elixir
29
star
45

gamesdk-and-dispatch

Public issue tracker for the Discord Game SDK and Dispatch
22
star
46

dispenser

Elixir library to buffer and send events to subscribers.
Elixir
17
star
47

eslint-plugin-discord

Custom ESLint rules for Discord
JavaScript
16
star
48

chromium-build

Python
15
star
49

hash_ring

hash_ring
C
14
star
50

limited_queue

Simple Elixir queue, with a constant-time `size/1` and a maximum capacity
Elixir
13
star
51

perceptual

A smarter volume slider scale
TypeScript
13
star
52

discord_vigilante

12
star
53

heroku-sample-app

Example discord bot using Heroku
JavaScript
11
star
54

postcss-theme-shorthand

Converts `light-` and `dark-` prefixed CSS properties into corresponding light/dark theme globals
JavaScript
11
star
55

babel-plugin-strip-object-freeze

Replace all instances of Object.freeze(value) with value
JavaScript
10
star
56

libyuv

our fork of libyuv for webrtc
C++
10
star
57

lilliput-rs

Lilliput, in Rust!
Rust
9
star
58

lilliput-bench

Benchmarker for lilliput
Python
8
star
59

sqlite3

Mirror of sqlite amalgamation from https://www.sqlite.org/
C
7
star
60

openh264

C++
6
star
61

slate-react-package-fork

TypeScript
6
star
62

react-native-video

A <Video /> component for react-native
Java
6
star
63

rlottiebinding-ios

rlottie ios submodule
Starlark
5
star
64

jemalloc_info

A small library for exporting jemalloc allocation data in Elixir
Elixir
5
star
65

libnice

Fork of https://nice.freedesktop.org/wiki/
C
5
star
66

libvpx

C
4
star
67

libsrtp

Fork of libsrtp
C
4
star
68

RLottieAndroid

C++
4
star
69

opentelemetry-rust-datadog

Rust
4
star
70

slate-package-fork

JavaScript
4
star
71

rust-protobuf

Rust implementation of Google protocol buffers
Rust
4
star
72

lilliput-dep-source

Convenient source repo for Lilliput's dependencies
3
star
73

slate-hotkeys-package-fork

3
star
74

rules_avro

🍃 bazel rules for generating code from avro schemas
Starlark
3
star
75

rules_ios

Bazel rules for building iOS applications and frameworks
Starlark
2
star
76

cocoapods-bazel

A Cocoapods plugin for automatically generating Bazel BUILD files
Ruby
2
star
77

eslint-plugin-react-discord

Fork of eslint-plugin-react
JavaScript
1
star