Skip to content

Commit

Permalink
Sample script, and doc cleanup.
Browse files Browse the repository at this point in the history
  • Loading branch information
cburschka committed Oct 30, 2016
1 parent f8309ef commit 8d96d2c
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 335 deletions.
89 changes: 48 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,84 @@
xbbcode.js
==========

This is a JavaScript version of my xbbcode module for Drupal.
This is a JavaScript version of [XBBCode](https://drupal.org/project/xbbcode).

xbbcode parses arbitrary text to find a properly nested tree of BBCode tags in square brackets,
and renders these tags according to markup strings and rendering callbacks that it has been
given during parser creation.
xbbcode parses arbitrary text to find a properly nested tree of BBCode tags in
square brackets, and renders these tags according to markup strings and
rendering callbacks that it has been given during parser creation.

Usage
=====

xbbcode.js creates a global function named `xbbcode`. To create a parser,
this function is invoked with a single argument containing the mapping of BBCode
tags to output, in the form of `tagEngine` described below:
xbbcode.js creates an XBBCode class, which is also its only export.
(This way, the module can be used both via `require()` and by simple inclusion
in a web page.)

tagEngine ::= {object} { tagName : (renderer | extended) }
tagName ::= {string} (must match /\w+/)
extended ::= {object} {
"body" : renderer
[, "selfclosing" : {bool} ]
[, "nocode" : {bool} ]
}
renderer ::= {string} | {function}
To create a parser, this class is instantiated with a single argument
containing the mapping of BBCode tags to output, in the form of `plugins`
described below:

plugins ::= { name : renderer }*
name ::= string (must match /\w+/)
renderer ::= string | function

A string renderer consists of any text with the following optional placeholders:
`{content}`, `{option}`, `{name}`, or `{attr:attributeName}` for any attribute name.
The following examples show what part of the input these placeholders will be replaced with.
A string renderer consists of any text with the following optional placeholders:
`{content}`, `{option}`, `{name}`, `{attribute.*}` or `{source} for any attribute name.

* `[{name}={option}]{content}[/{name}]`
* `[{name} attributeName="{attr:attributeName}" attribute2="{attr:attribute2}"]{content}][/{name}]`
* In `[tag1=abc]xyz[/tag1]`, `{option}` is `abc` and `{content}` is `xyz`.
* In `[tag2 abc="xyz"][tag3][/tag3][/tag2]`, `{attribute.abc}` is `xyz`,
`{source}` is `[tag3][/tag3]`, and `{content}` is whatever `[tag3][/tag3]`
is rendered as.

A function renderer receives a single argument, which is an object with the following properties:
`content`, `option`, `name`, and `attrs`. The first three are the same as above;
`attrs` is an object containing every attribute as a property.
The `{name}` placeholder is replaced with the tag name.

A function renderer receives a BBCodeElement instance as its only argument.
This object has the following methods, whose return values match the values
in the previous section:

- getName()
- getContent()
- getOption()
- getAttribute(key)
- getSource()

Sample
------

bbCodeTags = {
b: '<strong>{content}</strong>',
const XBBCode = require('xbbcode');
const filter = XBBCode.create({
b: '<strong>{content}</strong>',
quote: '<q>{content}</q>',
code: '<code>{content}</code>',
url: function (tag) {
return '<a href="'
+ 'http://example.com/outgoing?url='
+ encodeURI(tag.option)
+ '">' + tag.content + '</a>';
},
img: {
body: '<img src="{content}" alt="Image({content}" />',
nocode: true
}
};
bbcodeParser = XBBCode(bbCodeTags);
code: '<code>{content}</code>',
url: tag =>
(`<a href="http://example.com/outgoing?url=${encodeURI(tag.getOption())}">`
+ tag.getContent()
+ '</a>'),
img: '<img src="{source}" alt="Image({source}" />',
});

input = '[quote][code][url=http://example.org/][img]http://example.org/[b]image[/b].png[/img][/code][/url][/quote]';
console.log(bbcodeParser.render(input);
console.log(filter(input));

// Note that the [url] tag is improperly nested; it closes after the [code] tag.
// It will therefore not be rendered, to avoid generating improperly
// nested output markup.
// The [img] tag is set not to allow BBCode inside it,
// Which prevents the [b] tag from being rendered.
>>> "<q><code>[url=http://example.org/]<img src="http://example.org/[b]image[/b].png" /></a></code>[/url]</q>"
// "<q><code>[url=http://example.org/]<img src="http://example.org/[b]image[/b].png" /></a></code>[/url]</q>"

The recommended way to use xbbcode.js is to import the `src/xbbcode.js` file.

On legacy platforms, it may be necessary to run `make`, which uses Babel to
translate the file to a pre-ES2015 syntax, and then use `./xbbcode.js` in the
root folder.

LICENSE
=======

The MIT License (MIT)

Copyright (c) 2014-2015 Christoph Burschka
Copyright (c) 2014-2016 Christoph Burschka

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
10 changes: 10 additions & 0 deletions sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const XBBCode = require('./src/xbbcode.js');

const filter = XBBCode.create({
b: '<strong>{content}</strong>',
url: tag => `<a href="${tag.getOption()}">${tag.getContent()}</a>`,
});

const input = 'Hello [b][url=https://github.com/cburschka/xbbcode.js]world[/url][/b].';
console.log(">>>", input);
console.log(filter(input));
97 changes: 56 additions & 41 deletions src/xbbcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,25 @@ const XBBCode = (() => {
/**
* Generate a new XBBCode parser.
*
* @param tags: {object} (tagName : (renderer | extended))
* tagName: {string}
* renderer: {string} | {function}
* extended: {object} {
* "renderer": renderer,
* ["selfclosing": {bool} ,]
* ["nocode": {bool} ,]
* }
* @param tags An object keyed by tag name, the values being strings or functions.
*
* The render function will receive a tag object with the keys
* "content", "option", "attrs" (keyed by attribute name) and "name".
* the render string may contain the placeholders {content}, {option}, {name},
* or any attribute key.
* A render function will receive a BBCodeElement object with the methods
* getContent(), getSource(), getAttribute(), getOption() and getName().
* A template string may contain the placeholders
* {content}, {source}, {attribute.*}, {option} and {name}.
*/
const XBBCode = class {
static create(plugins) {
const processor = new XBBCode(plugins);
return text => processor.process(text);
}

constructor(plugins) {
this.plugins = plugins;
}

render(text) {
const tags = this.findTags(text);
const tree = this.buildTree(tags, text);
return tree.render();
process(text) {
return this.parser(this.lexer(text)).getContent();
}

lexer(text) {
Expand All @@ -41,22 +37,19 @@ const XBBCode = (() => {
let last = 0;
while (match = pattern.exec(text)) {
const {index} = match;
const [tag, close, name, _, attributes] = match;
const [tag, close, name, _, option, attributes] = match;
if (!this.plugins[name]) return;
tokens.push(
text.substring(last, index),
{
tag, name, option,
open: !!close,
attributes: BBCodeElement.parseAttributes(attributes),
}
{tag, name, option, attributes, open: !close}
);
last = index + tag.length;
}
tokens.push(text.substring(last));
return tokens;
}

parse(tokens) {
parser(tokens) {
// Initialize tag counter.
const countOpen = {};
tokens.forEach(token => {
Expand All @@ -78,11 +71,11 @@ const XBBCode = (() => {
// Found a closing tag that matches an open one.
else if (countOpen[name]) {
// Break all dangling tags inside the one that just closed.
while (stack.last().name != name) {
while (stack.last().getName() != name) {
// Break a tag by appending its element and content to its parent.
const broken = stack.pop();
stack.last().append(broken.tag, broken.children);
countOpen[broken.name]--;
stack.last().append(...broken.break());
countOpen[broken.getName()]--;
}

const closed = stack.pop();
Expand All @@ -94,18 +87,21 @@ const XBBCode = (() => {
// Break the dangling open tags.
while (stack.length > 1) {
const broken = stack.pop();
stack.last().append(broken.tag, broken.children);
stack.last().append(...broken.break());
}
return stack[0];
}
}

const BBCodeElement = class {
constructor(plugin, token) {
this.children = [];
this.plugin = plugin;
this.token = token;
this.name = token.name;
this.children = [];
}

break() {
return [this.token.tag, ...this.children];
}

getContent() {
Expand All @@ -115,6 +111,19 @@ const XBBCode = (() => {
return this.content;
}

getName() {
return this.token.name;
}

getOption() {
return this.token.option;
}

getAttribute(key) {
if (!this.attributes) this.parseAttributes();
return this.attributes[key];
}

getSource() {
if (this.source === undefined) this.source = this.children
.map(child => child.getSource ? child.getSource() : child)
Expand All @@ -123,7 +132,7 @@ const XBBCode = (() => {
}

append(...children) {
children.push(...[].concat(children));
this.children.push(...children);
}

render() {
Expand All @@ -132,26 +141,28 @@ const XBBCode = (() => {
if (typeof renderer === 'string') {
// Replace placeholders of the form {x}, but allow escaping
// literal braces with {{x}}.
return renderer.replace(/\{(?:(attribute:)?(\w+)|(\{\w+\}))\}/g, function(_, attr, key, escape) {
return renderer.replace(/\{(?:(attribute\.)?(\w+)|(\{\w+\}))\}/g, (_, attr, key, escape) => {
if (escape) return escape;
if (attr) return this.attributes[key] || '';
if (key == 'content') return this.getContent();
if (key == 'name') return this.name;
if (key == 'option') return this.option || '';
if (attr) return this.getAttribute(key) || '';
switch (key) {
case 'content': return this.getContent();
case 'name': return this.getName();
case 'option': return this.getOption();
case 'source': return this.getSource();
}
return '';
});
}
}

static parseAttributes(text) {
const attributes = {};
parseAttributes() {
this.attributes = {};
const pattern = new RegExp(RE_ATTRIBUTE, 'gi');
let match;
while (match = pattern.exec(text)) {
while (match = pattern.exec(this.token.attributes)) {
const [_, key, __, value] = match;
attributes[key] = value;
this.attributes[key] = value;
}
return attributes;
}
}

Expand All @@ -176,6 +187,10 @@ const XBBCode = (() => {
}
}

if (module) module.exports = {XBBCode, BBCodeElement}
return XBBCode;
})();

try {
module.exports = XBBCode;
}
catch (e) {}
Loading

0 comments on commit 8d96d2c

Please sign in to comment.