2012-12-13 21:34:31 +00:00
/ * \
title : $ : / c o r e / m o d u l e s / p a r s e r s / w i k i p a r s e r / w i k i p a r s e r . j s
type : application / javascript
2013-01-16 13:56:11 +00:00
module - type : parser
2012-12-13 21:34:31 +00:00
The wiki text parser processes blocks of source text into a parse tree .
The parse tree is made up of nested arrays of these JavaScript objects :
{ type : "element" , tag : < string > , attributes : { } , children : [ ] } - an HTML element
{ type : "text" , text : < string > } - a text node
{ type : "entity" , value : < string > } - an entity
{ type : "raw" , html : < string > } - raw HTML
Attributes are stored as hashmaps of the following objects :
{ type : "string" , value : < string > } - literal string
{ type : "indirect" , textReference : < textReference > } - indirect through a text reference
\ * /
( function ( ) {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict" ;
2012-12-26 22:02:59 +00:00
var WikiParser = function ( type , text , options ) {
2012-12-13 21:34:31 +00:00
this . wiki = options . wiki ;
2015-08-29 15:33:04 +00:00
var self = this ;
// Check for an externally linked tiddler
if ( $tw . browser && options . _canonical _uri ) {
$tw . utils . httpRequest ( {
url : options . _canonical _uri ,
type : "GET" ,
callback : function ( err , data ) {
if ( ! err ) {
var tiddlers = self . wiki . deserializeTiddlers ( ".tid" , data , self . wiki . getCreationFields ( ) )
if ( tiddlers ) {
self . wiki . addTiddlers ( tiddlers ) ;
}
}
}
} ) ;
text = "Loading external text from ''" + options . _canonical _uri + "''\n\nIf this message doesn't disappear you may be using a browser that doesn't support external text in this configuration. See http://tiddlywiki.com/#ExternalText" ;
}
2012-12-26 22:02:59 +00:00
// Initialise the classes if we don't have them already
if ( ! this . pragmaRuleClasses ) {
WikiParser . prototype . pragmaRuleClasses = $tw . modules . createClassesFromModules ( "wikirule" , "pragma" , $tw . WikiRuleBase ) ;
2015-07-30 11:28:29 +00:00
this . setupRules ( WikiParser . prototype . pragmaRuleClasses , "$:/config/WikiParserRules/Pragmas/" ) ;
2012-12-26 22:02:59 +00:00
}
if ( ! this . blockRuleClasses ) {
WikiParser . prototype . blockRuleClasses = $tw . modules . createClassesFromModules ( "wikirule" , "block" , $tw . WikiRuleBase ) ;
2015-07-30 11:28:29 +00:00
this . setupRules ( WikiParser . prototype . blockRuleClasses , "$:/config/WikiParserRules/Block/" ) ;
2012-12-26 22:02:59 +00:00
}
if ( ! this . inlineRuleClasses ) {
WikiParser . prototype . inlineRuleClasses = $tw . modules . createClassesFromModules ( "wikirule" , "inline" , $tw . WikiRuleBase ) ;
2015-07-30 11:28:29 +00:00
this . setupRules ( WikiParser . prototype . inlineRuleClasses , "$:/config/WikiParserRules/Inline/" ) ;
2012-12-26 22:02:59 +00:00
}
2012-12-13 21:34:31 +00:00
// Save the parse text
this . type = type || "text/vnd.tiddlywiki" ;
this . source = text || "" ;
this . sourceLength = this . source . length ;
// Set current parse position
this . pos = 0 ;
// Instantiate the pragma parse rules
2012-12-26 22:02:59 +00:00
this . pragmaRules = this . instantiateRules ( this . pragmaRuleClasses , "pragma" , 0 ) ;
2012-12-26 19:35:54 +00:00
// Instantiate the parser block and inline rules
2012-12-26 22:02:59 +00:00
this . blockRules = this . instantiateRules ( this . blockRuleClasses , "block" , 0 ) ;
this . inlineRules = this . instantiateRules ( this . inlineRuleClasses , "inline" , 0 ) ;
2012-12-13 21:34:31 +00:00
// Parse any pragmas
2015-07-05 16:48:18 +00:00
this . tree = [ ] ;
var topBranch = this . parsePragmas ( ) ;
2012-12-20 12:18:38 +00:00
// Parse the text into inline runs or blocks
2012-12-20 15:07:38 +00:00
if ( options . parseAsInline ) {
2015-07-05 16:48:18 +00:00
topBranch . push . apply ( topBranch , this . parseInlineRun ( ) ) ;
2012-12-13 21:34:31 +00:00
} else {
2015-07-05 16:48:18 +00:00
topBranch . push . apply ( topBranch , this . parseBlocks ( ) ) ;
2012-12-13 21:34:31 +00:00
}
2012-12-29 23:17:09 +00:00
// Return the parse tree
2012-12-13 21:34:31 +00:00
} ;
2015-07-30 11:28:29 +00:00
/ *
* /
WikiParser . prototype . setupRules = function ( proto , configPrefix ) {
var self = this ;
2015-07-31 12:33:33 +00:00
if ( ! $tw . safemode ) {
$tw . utils . each ( proto , function ( object , name ) {
if ( self . wiki . getTiddlerText ( configPrefix + name , "enable" ) !== "enable" ) {
delete proto [ name ] ;
}
} ) ;
}
2015-07-30 11:28:29 +00:00
} ;
2012-12-13 21:34:31 +00:00
/ *
Instantiate an array of parse rules
* /
2012-12-20 16:49:04 +00:00
WikiParser . prototype . instantiateRules = function ( classes , type , startPos ) {
2012-12-14 13:31:47 +00:00
var rulesInfo = [ ] ,
2012-12-13 21:34:31 +00:00
self = this ;
$tw . utils . each ( classes , function ( RuleClass ) {
// Instantiate the rule
2012-12-14 13:31:47 +00:00
var rule = new RuleClass ( self ) ;
2012-12-20 16:49:04 +00:00
rule . is = { } ;
rule . is [ type ] = true ;
2012-12-14 15:44:19 +00:00
rule . init ( self ) ;
2012-12-14 13:31:47 +00:00
var matchIndex = rule . findNextMatch ( startPos ) ;
if ( matchIndex !== undefined ) {
rulesInfo . push ( {
rule : rule ,
matchIndex : matchIndex
} ) ;
2012-12-13 21:34:31 +00:00
}
} ) ;
2012-12-14 13:31:47 +00:00
return rulesInfo ;
2012-12-13 21:34:31 +00:00
} ;
/ *
Skip any whitespace at the current position . Options are :
treatNewlinesAsNonWhitespace : true if newlines are NOT to be treated as whitespace
* /
WikiParser . prototype . skipWhitespace = function ( options ) {
options = options || { } ;
var whitespaceRegExp = options . treatNewlinesAsNonWhitespace ? /([^\S\n]+)/mg : /(\s+)/mg ;
whitespaceRegExp . lastIndex = this . pos ;
var whitespaceMatch = whitespaceRegExp . exec ( this . source ) ;
if ( whitespaceMatch && whitespaceMatch . index === this . pos ) {
this . pos = whitespaceRegExp . lastIndex ;
}
} ;
/ *
Get the next match out of an array of parse rule instances
* /
WikiParser . prototype . findNextMatch = function ( rules , startPos ) {
2012-12-14 13:31:47 +00:00
// Find the best matching rule by finding the closest match position
2014-08-30 19:44:26 +00:00
var matchingRule ,
2012-12-14 13:31:47 +00:00
matchingRulePos = this . sourceLength ;
// Step through each rule
2012-12-13 21:34:31 +00:00
for ( var t = 0 ; t < rules . length ; t ++ ) {
2012-12-14 13:31:47 +00:00
var ruleInfo = rules [ t ] ;
// Ask the rule to get the next match if we've moved past the current one
if ( ruleInfo . matchIndex !== undefined && ruleInfo . matchIndex < startPos ) {
ruleInfo . matchIndex = ruleInfo . rule . findNextMatch ( startPos ) ;
}
// Adopt this match if it's closer than the current best match
if ( ruleInfo . matchIndex !== undefined && ruleInfo . matchIndex <= matchingRulePos ) {
matchingRule = ruleInfo ;
matchingRulePos = ruleInfo . matchIndex ;
2012-12-13 21:34:31 +00:00
}
}
2012-12-14 13:31:47 +00:00
return matchingRule ;
2012-12-13 21:34:31 +00:00
} ;
/ *
Parse any pragmas at the beginning of a block of parse text
* /
WikiParser . prototype . parsePragmas = function ( ) {
2015-07-05 16:48:18 +00:00
var currentTreeBranch = this . tree ;
2012-12-13 21:34:31 +00:00
while ( true ) {
// Skip whitespace
this . skipWhitespace ( ) ;
// Check for the end of the text
if ( this . pos >= this . sourceLength ) {
2012-12-29 23:17:09 +00:00
break ;
2012-12-13 21:34:31 +00:00
}
// Check if we've arrived at a pragma rule match
var nextMatch = this . findNextMatch ( this . pragmaRules , this . pos ) ;
// If not, just exit
if ( ! nextMatch || nextMatch . matchIndex !== this . pos ) {
2012-12-29 23:17:09 +00:00
break ;
2012-12-13 21:34:31 +00:00
}
// Process the pragma rule
2015-07-05 16:48:18 +00:00
var subTree = nextMatch . rule . parse ( ) ;
if ( subTree . length > 0 ) {
// Quick hack; we only cope with a single parse tree node being returned, which is true at the moment
currentTreeBranch . push . apply ( currentTreeBranch , subTree ) ;
subTree [ 0 ] . children = [ ] ;
currentTreeBranch = subTree [ 0 ] . children ;
}
2012-12-13 21:34:31 +00:00
}
2015-07-05 16:48:18 +00:00
return currentTreeBranch ;
2012-12-13 21:34:31 +00:00
} ;
/ *
Parse a block from the current position
terminatorRegExpString : optional regular expression string that identifies the end of plain paragraphs . Must not include capturing parenthesis
* /
WikiParser . prototype . parseBlock = function ( terminatorRegExpString ) {
var terminatorRegExp = terminatorRegExpString ? new RegExp ( "(" + terminatorRegExpString + "|\\r?\\n\\r?\\n)" , "mg" ) : /(\r?\n\r?\n)/mg ;
this . skipWhitespace ( ) ;
if ( this . pos >= this . sourceLength ) {
return [ ] ;
}
// Look for a block rule that applies at the current position
var nextMatch = this . findNextMatch ( this . blockRules , this . pos ) ;
if ( nextMatch && nextMatch . matchIndex === this . pos ) {
2012-12-14 13:31:47 +00:00
return nextMatch . rule . parse ( ) ;
2012-12-13 21:34:31 +00:00
}
// Treat it as a paragraph if we didn't find a block rule
2012-12-20 12:18:38 +00:00
return [ { type : "element" , tag : "p" , children : this . parseInlineRun ( terminatorRegExp ) } ] ;
2012-12-13 21:34:31 +00:00
} ;
/ *
Parse a series of blocks of text until a terminating regexp is encountered or the end of the text
terminatorRegExpString : terminating regular expression
* /
WikiParser . prototype . parseBlocks = function ( terminatorRegExpString ) {
if ( terminatorRegExpString ) {
return this . parseBlocksTerminated ( terminatorRegExpString ) ;
} else {
return this . parseBlocksUnterminated ( ) ;
}
} ;
/ *
Parse a block from the current position to the end of the text
* /
WikiParser . prototype . parseBlocksUnterminated = function ( ) {
var tree = [ ] ;
while ( this . pos < this . sourceLength ) {
tree . push . apply ( tree , this . parseBlock ( ) ) ;
}
return tree ;
} ;
/ *
Parse blocks of text until a terminating regexp is encountered
* /
WikiParser . prototype . parseBlocksTerminated = function ( terminatorRegExpString ) {
var terminatorRegExp = new RegExp ( "(" + terminatorRegExpString + ")" , "mg" ) ,
tree = [ ] ;
// Skip any whitespace
this . skipWhitespace ( ) ;
// Check if we've got the end marker
terminatorRegExp . lastIndex = this . pos ;
var match = terminatorRegExp . exec ( this . source ) ;
// Parse the text into blocks
while ( this . pos < this . sourceLength && ! ( match && match . index === this . pos ) ) {
var blocks = this . parseBlock ( terminatorRegExpString ) ;
tree . push . apply ( tree , blocks ) ;
// Skip any whitespace
this . skipWhitespace ( ) ;
// Check if we've got the end marker
terminatorRegExp . lastIndex = this . pos ;
match = terminatorRegExp . exec ( this . source ) ;
}
if ( match && match . index === this . pos ) {
this . pos = match . index + match [ 0 ] . length ;
}
return tree ;
} ;
/ *
Parse a run of text at the current position
terminatorRegExp : a regexp at which to stop the run
2012-12-15 11:38:28 +00:00
options : see below
Options available :
eatTerminator : move the parse position past any encountered terminator ( default false )
2012-12-13 21:34:31 +00:00
* /
2012-12-20 12:18:38 +00:00
WikiParser . prototype . parseInlineRun = function ( terminatorRegExp , options ) {
2012-12-13 21:34:31 +00:00
if ( terminatorRegExp ) {
2012-12-20 12:18:38 +00:00
return this . parseInlineRunTerminated ( terminatorRegExp , options ) ;
2012-12-13 21:34:31 +00:00
} else {
2012-12-20 12:18:38 +00:00
return this . parseInlineRunUnterminated ( options ) ;
2012-12-13 21:34:31 +00:00
}
} ;
2012-12-20 12:18:38 +00:00
WikiParser . prototype . parseInlineRunUnterminated = function ( options ) {
2012-12-13 21:34:31 +00:00
var tree = [ ] ;
2012-12-20 15:07:38 +00:00
// Find the next occurrence of an inline rule
var nextMatch = this . findNextMatch ( this . inlineRules , this . pos ) ;
2012-12-13 21:34:31 +00:00
// Loop around the matches until we've reached the end of the text
while ( this . pos < this . sourceLength && nextMatch ) {
// Process the text preceding the run rule
if ( nextMatch . matchIndex > this . pos ) {
tree . push ( { type : "text" , text : this . source . substring ( this . pos , nextMatch . matchIndex ) } ) ;
this . pos = nextMatch . matchIndex ;
}
// Process the run rule
2012-12-14 13:31:47 +00:00
tree . push . apply ( tree , nextMatch . rule . parse ( ) ) ;
2012-12-13 21:34:31 +00:00
// Look for the next run rule
2012-12-20 15:07:38 +00:00
nextMatch = this . findNextMatch ( this . inlineRules , this . pos ) ;
2012-12-13 21:34:31 +00:00
}
// Process the remaining text
if ( this . pos < this . sourceLength ) {
tree . push ( { type : "text" , text : this . source . substr ( this . pos ) } ) ;
}
this . pos = this . sourceLength ;
return tree ;
} ;
2012-12-20 12:18:38 +00:00
WikiParser . prototype . parseInlineRunTerminated = function ( terminatorRegExp , options ) {
2012-12-15 11:38:28 +00:00
options = options || { } ;
2012-12-13 21:34:31 +00:00
var tree = [ ] ;
// Find the next occurrence of the terminator
terminatorRegExp . lastIndex = this . pos ;
var terminatorMatch = terminatorRegExp . exec ( this . source ) ;
2012-12-20 12:18:38 +00:00
// Find the next occurrence of a inlinerule
var inlineRuleMatch = this . findNextMatch ( this . inlineRules , this . pos ) ;
2012-12-13 21:34:31 +00:00
// Loop around until we've reached the end of the text
2012-12-20 12:18:38 +00:00
while ( this . pos < this . sourceLength && ( terminatorMatch || inlineRuleMatch ) ) {
// Return if we've found the terminator, and it precedes any inline rule match
2012-12-13 21:34:31 +00:00
if ( terminatorMatch ) {
2012-12-20 12:18:38 +00:00
if ( ! inlineRuleMatch || inlineRuleMatch . matchIndex >= terminatorMatch . index ) {
2012-12-13 21:34:31 +00:00
if ( terminatorMatch . index > this . pos ) {
tree . push ( { type : "text" , text : this . source . substring ( this . pos , terminatorMatch . index ) } ) ;
}
this . pos = terminatorMatch . index ;
2012-12-15 11:38:28 +00:00
if ( options . eatTerminator ) {
this . pos += terminatorMatch [ 0 ] . length ;
}
2012-12-13 21:34:31 +00:00
return tree ;
}
}
2012-12-20 12:18:38 +00:00
// Process any inline rule, along with the text preceding it
if ( inlineRuleMatch ) {
2012-12-13 21:34:31 +00:00
// Preceding text
2012-12-20 12:18:38 +00:00
if ( inlineRuleMatch . matchIndex > this . pos ) {
tree . push ( { type : "text" , text : this . source . substring ( this . pos , inlineRuleMatch . matchIndex ) } ) ;
this . pos = inlineRuleMatch . matchIndex ;
2012-12-13 21:34:31 +00:00
}
2012-12-20 12:18:38 +00:00
// Process the inline rule
tree . push . apply ( tree , inlineRuleMatch . rule . parse ( ) ) ;
// Look for the next inline rule
inlineRuleMatch = this . findNextMatch ( this . inlineRules , this . pos ) ;
2012-12-13 21:34:31 +00:00
// Look for the next terminator match
terminatorRegExp . lastIndex = this . pos ;
terminatorMatch = terminatorRegExp . exec ( this . source ) ;
}
}
// Process the remaining text
if ( this . pos < this . sourceLength ) {
tree . push ( { type : "text" , text : this . source . substr ( this . pos ) } ) ;
}
this . pos = this . sourceLength ;
return tree ;
} ;
/ *
2012-12-15 17:35:16 +00:00
Parse zero or more class specifiers ` .classname `
2012-12-13 21:34:31 +00:00
* /
2012-12-15 17:35:16 +00:00
WikiParser . prototype . parseClasses = function ( ) {
2012-12-13 21:34:31 +00:00
var classRegExp = /\.([^\s\.]+)/mg ,
classNames = [ ] ;
classRegExp . lastIndex = this . pos ;
var match = classRegExp . exec ( this . source ) ;
while ( match && match . index === this . pos ) {
this . pos = match . index + match [ 0 ] . length ;
classNames . push ( match [ 1 ] ) ;
2014-08-30 19:44:26 +00:00
match = classRegExp . exec ( this . source ) ;
2012-12-13 21:34:31 +00:00
}
2012-12-15 17:35:16 +00:00
return classNames ;
2012-12-13 21:34:31 +00:00
} ;
2012-12-26 19:35:54 +00:00
/ *
Amend the rules used by this instance of the parser
type : ` only ` keeps just the named rules , ` except ` keeps all but the named rules
names : array of rule names
* /
WikiParser . prototype . amendRules = function ( type , names ) {
names = names || [ ] ;
// Define the filter function
var keepFilter ;
if ( type === "only" ) {
keepFilter = function ( name ) {
return names . indexOf ( name ) !== - 1 ;
} ;
} else if ( type === "except" ) {
keepFilter = function ( name ) {
return names . indexOf ( name ) === - 1 ;
} ;
} else {
return ;
}
// Define a function to process each of our rule arrays
var processRuleArray = function ( ruleArray ) {
for ( var t = ruleArray . length - 1 ; t >= 0 ; t -- ) {
if ( ! keepFilter ( ruleArray [ t ] . rule . name ) ) {
ruleArray . splice ( t , 1 ) ;
}
}
} ;
// Process each rule array
processRuleArray ( this . pragmaRules ) ;
processRuleArray ( this . blockRules ) ;
processRuleArray ( this . inlineRules ) ;
2014-08-30 19:44:26 +00:00
} ;
2012-12-26 19:35:54 +00:00
2012-12-27 17:08:29 +00:00
exports [ "text/vnd.tiddlywiki" ] = WikiParser ;
2012-12-13 21:34:31 +00:00
} ) ( ) ;