Skip to content

Commit

Permalink
Add support for wikilink and external link using mediawiki style syntax
Browse files Browse the repository at this point in the history
Tests included
  • Loading branch information
santhoshtr committed Apr 28, 2020
1 parent f37d3e1 commit 8203e3f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 18 deletions.
139 changes: 122 additions & 17 deletions src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
* @param {string} message
*/
export default function BananaMessage (message) {
let pipe, colon, backslash, anyCharacter, dollar, digits, regularLiteral,
regularLiteralWithoutBar, regularLiteralWithoutSpace, escapedOrLiteralWithoutBar,
escapedOrRegularLiteral, templateContents, templateName, openTemplate,
closeTemplate, expression, paramExpression, result
let escapedOrLiteralWithoutBar,
escapedOrRegularLiteral, templateContents, templateName,
expression, paramExpression, result

let pos = 0

Expand Down Expand Up @@ -104,15 +103,16 @@ export default function BananaMessage (message) {
}
}

pipe = makeStringParser('|')
colon = makeStringParser(':')
backslash = makeStringParser('\\')
anyCharacter = makeRegexParser(/^./)
dollar = makeStringParser('$')
digits = makeRegexParser(/^\d+/)
regularLiteral = makeRegexParser(/^[^{}[\]$\\]/)
regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/)
regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/)
const whitespace = makeRegexParser(/^\s+/)
const pipe = makeStringParser('|')
const colon = makeStringParser(':')
const backslash = makeStringParser('\\')
const anyCharacter = makeRegexParser(/^./)
const dollar = makeStringParser('$')
const digits = makeRegexParser(/^\d+/)
const regularLiteral = makeRegexParser(/^[^{}[\]$\\]/)
const regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/)
const regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/)

// There is a general pattern:
// parse a thing;
Expand All @@ -137,9 +137,22 @@ export default function BananaMessage (message) {
return result === null ? null : result.join('')
}

// Used to define "literals" within template parameters.
// The pipe character is the parameter delimeter, so by default
// it is not a literal in the parameter
function literal () {
let result = nOrMore(1, escapedOrRegularLiteral)()
const result = nOrMore(1, escapedOrRegularLiteral)()
return result === null ? null : result.join('')
}

const escapedOrLiteralWithoutSpace = choice([
escapedLiteral,
regularLiteralWithoutSpace
])

// Used to define "literals" without spaces, in space-delimited situations
function literalWithoutSpace () {
const result = nOrMore(1, escapedOrLiteralWithoutSpace)()
return result === null ? null : result.join('')
}

Expand Down Expand Up @@ -223,16 +236,108 @@ export default function BananaMessage (message) {
}
])

openTemplate = makeStringParser('{{')
closeTemplate = makeStringParser('}}')
const openTemplate = makeStringParser('{{')
const closeTemplate = makeStringParser('}}')
const openWikilink = makeStringParser('[[')
const closeWikilink = makeStringParser(']]')
const openExtlink = makeStringParser('[')

This comment has been minimized.

Copy link
@stephanebisson

stephanebisson Sep 17, 2020

This is crashing the KaiOS app since we have a message with brackets (Reference [$1] for the reference preview dialog title)

Turning some syntax from inert to active is a major breaking change for anyone who happens to use this syntax for its literal value. Would this warrant a major version update?

This comment has been minimized.

Copy link
@santhoshtr

santhoshtr Sep 18, 2020

Author Member

Hi, since the intention of library is to support messages as per mediawiki spec, I considered this as a missing part and hence a bug fix. That is why the reason for not upgrading the major version. But I see your point. I think the issue you have is using external link syntax unescaped in i18n message. Translators will also believe that it is an external link because that is how wikitext markup works. Some of the discussion at #37 are related to this.

If you were using mediawiki, you would write the message like Reference [$1] or use wfEscapeWikiText

const closeExtlink = makeStringParser(']')

function template () {
let result = sequence([ openTemplate, templateContents, closeTemplate ])

return result === null ? null : result[ 1 ]
}

expression = choice([ template, replacement, literal ])
function pipedWikilink () {
var result = sequence([
nOrMore(1, paramExpression),
pipe,
nOrMore(1, expression)
])
return result === null ? null : [
[ 'CONCAT' ].concat(result[ 0 ]),
[ 'CONCAT' ].concat(result[ 2 ])
]
}

function unpipedWikilink () {
var result = sequence([
nOrMore(1, paramExpression)
])
return result === null ? null : [
[ 'CONCAT' ].concat(result[ 0 ])
]
}

const wikilinkContents = choice([
pipedWikilink,
unpipedWikilink
])

function wikilink () {
let result = null

const parsedResult = sequence([
openWikilink,
wikilinkContents,
closeWikilink
])

if (parsedResult !== null) {
const parsedLinkContents = parsedResult[ 1 ]
result = [ 'WIKILINK' ].concat(parsedLinkContents)
}

return result
}

// this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
function extlink () {
let result = null

const parsedResult = sequence([
openExtlink,
nOrMore(1, nonWhitespaceExpression),
whitespace,
nOrMore(1, expression),
closeExtlink
])

if (parsedResult !== null) {
// When the entire link target is a single parameter, we can't use CONCAT, as we allow
// passing fancy parameters (like a whole jQuery object or a function) to use for the
// link. Check only if it's a single match, since we can either do CONCAT or not for
// singles with the same effect.
const target = parsedResult[ 1 ].length === 1
? parsedResult[ 1 ][ 0 ]
: [ 'CONCAT' ].concat(parsedResult[ 1 ])
result = [
'EXTLINK',
target,
[ 'CONCAT' ].concat(parsedResult[ 3 ])
]
}

return result
}

const nonWhitespaceExpression = choice([
template,
replacement,
wikilink,
extlink,
literalWithoutSpace
])

expression = choice([
template,
replacement,
wikilink,
extlink,
literal
])

paramExpression = choice([ template, replacement, literalWithoutBar ])

function start () {
Expand Down
40 changes: 40 additions & 0 deletions src/emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,46 @@ class BananaEmitter {
return word && form && this.language.convertGrammar(word, form)
}

/**
* Transform wiki-link
*
* @param {String[]} nodes
* @return {String}
*/
wikilink (nodes) {
let anchor
let page = nodes[0]
// Strip leading ':', which is used to suppress special behavior in wikitext links,
// e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
if (page.charAt(0) === ':') {
page = page.slice(1)
}
const url = `./${page}`

if (nodes.length === 1) {
// [[Some Page]] or [[Namespace:Some Page]]
anchor = page
} else {
// [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
anchor = nodes[1]
}

return `<a href="${url}" title="${page}">${anchor}</a>`
}

/**
* Transform parsed structure into external link.
*
* @param {String[]} nodes
* @return {String}
*/
extlink (nodes) {
if (nodes.length !== 2) {
throw Error('Expected two items in the node')
}
return `<a href="${nodes[0]}">${nodes[1]}</a>`
}

/**
* Wraps argument with unicode control characters for directionality safety
*
Expand Down
2 changes: 1 addition & 1 deletion src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class BananaParser {
}

parse (message, params) {
if (message.includes('{{')) {
if (message.includes('{{') || message.includes('[')) {
let ast = new BananaMessage(message)
return this.emitter.emit(ast, params)
} else {
Expand Down
24 changes: 24 additions & 0 deletions test/banana.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,30 @@ describe('Banana', function () {
)
})

it('should localize the messages with wiki liinks', () => {
const banana = new Banana('en')
banana.load({
'msg-with-extlink': 'This is a link to [https://wikipedia.org wikipedia]',
'msg-with-wikilink': 'This is a link to [[Apple|Apple Page]]',
'msg-with-wikilink-no-anchor': 'This is a link to [[Apple]]'
}, 'en')
assert.strictEqual(
banana.i18n('msg-with-extlink'),
'This is a link to <a href="https://wikipedia.org">wikipedia</a>',
'External link'
)
assert.strictEqual(
banana.i18n('msg-with-wikilink'),
'This is a link to <a href="./Apple" title="Apple">Apple Page</a>',
'Internal Wiki style link with link and title being different'
)
assert.strictEqual(
banana.i18n('msg-with-wikilink-no-anchor'),
'This is a link to <a href="./Apple" title="Apple">Apple</a>',
'Internal Wiki style link with link and title being same'
)
})

for (var langCode in grammarTests) {
grammarTest(langCode, grammarTests[langCode])
}
Expand Down

0 comments on commit 8203e3f

Please sign in to comment.