Last active
March 4, 2016 15:11
-
-
Save geakstr/68cc92493feb166271ff to your computer and use it in GitHub Desktop.
Create "recursive" rules for ace editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Create "recursive" rules for ace editor | |
* | |
* Solve this question: https://github.com/ajaxorg/ace/issues/2861 | |
* | |
* Takes rules object which contains 3 type of rules: blocks, paired and unpaired. | |
* | |
* var rules = { | |
* blocks: { | |
* custom: "^\\+" // blocks with leading + will have "custom" class | |
* }, | |
* paired: { | |
* strong: "\\*", // *text* -> <span class="ace_strong">text</span> | |
* italic: "_" // _text_ -> <span class="ace_italic">text</span> | |
* }, | |
* unpaired: { // [email protected] -> <span class="ace_email">[email protected]</span> | |
* email: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}" | |
* } | |
* }; | |
* | |
* You must call it with ace highlighter context: extendRules.call(this, rules); | |
* | |
This text: | |
=========================================== | |
Lorem ipsum dolor sit amet, *consectetur _adipiscing_ elit*, sed do eiusmod tempor _incididunt_ ut labore et dolore magna aliqua. Ut enim ad minim veniam, *quis nostrud exercitation* ullamco laboris nisi ut aliquip ex ea commodo consequat. | |
+Duis aute irure *dolor in reprehenderit _in [email protected]_ voluptate velit esse* cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | |
=========================================== | |
Will be translated by ace to the next html: | |
=========================================== | |
<div class="ace_layer ace_text-layer"> | |
<div class="ace_line_group"> | |
<div class="ace_line">Lorem ipsum dolor sit amet, <span class="ace_strong">*consectetur </span><span class="ace_italic ace_strong">_adipiscing_</span><span class="ace_strong"> elit*</span>, sed do eiusmod tempor <span class="ace_italic">_incididunt_</span> ut labore et dolore magna </div> | |
<div class="ace_line">aliqua. Ut enim ad minim veniam, <span class="ace_strong">*quis nostrud exercitation*</span> ullamco laboris nisi ut aliquip ex ea commodo consequat.</div> | |
</div> | |
<div class="ace_line_group"> | |
<div class="ace_line"></div> | |
</div> | |
<div class="ace_line_group"> | |
<div class="ace_line"><span class="ace_custom">+Duis aute irure </span><span class="ace_strong ace_custom">*dolor in reprehenderit </span><span class="ace_italic ace_strong ace_custom">_in </span><span class="ace_email ace_italic ace_strong ace_custom">[email protected]</span><span class="ace_italic ace_strong ace_custom">_</span><span class="ace_strong ace_custom"> voluptate velit esse*</span><span class="ace_custom"> cillum dolore eu fugiat nulla pariatur. </span></div> | |
<div class="ace_line"><span class="ace_custom">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span></div> | |
</div> | |
</div> | |
=========================================== | |
*/ | |
export default function extendRules(rules) { | |
// Translate paired regexes to ace rules | |
for (let ruleName in rules.paired) { | |
rules.paired[ruleName] = pairedRuleFactory(ruleName, rules.paired[ruleName]); | |
} | |
// Translate unpaired regexes to ace rules | |
for (let ruleName in rules.unpaired) { | |
rules.unpaired[ruleName] = unpairedRuleFactory(ruleName, rules.unpaired[ruleName]); | |
} | |
// Rule for reset state while start of line | |
const startOfLineRule = { | |
regex: "^", | |
onMatch: function(val, state, stack) { | |
stack.length = 0; | |
stack.skipNext = false; | |
return (this.next = "start"); | |
} | |
}; | |
const backslashRule = { | |
regex: "\\\\", | |
onMatch: function(val, state, stack) { | |
stack.skipNext = !stack.skipNext; | |
return makeToken((this.next = state), stack); | |
} | |
}; | |
const defaultTokenRule = { | |
regex: ".", | |
onMatch: function(val, state, stack) { | |
stack.skipNext = false; | |
return makeToken(state, stack); | |
} | |
}; | |
this.$rules.start = [backslashRule]; | |
// First, setting up blocks (outer) rules | |
for (let blockRuleName in rules.blocks) { | |
this.$rules.start.push(makeBlockRule(blockRuleName, rules.blocks[blockRuleName])); | |
} | |
// Next extend block rules with inner rules | |
for (let blockRuleName in rules.blocks) { | |
this.$rules[blockRuleName] = [startOfLineRule, backslashRule]; | |
// Setting up paired rules | |
for (let ruleName in rules.paired) { | |
const ruleRegex = rules.paired[ruleName].regex; | |
const rule = makePairedRule(ruleName, ruleRegex); | |
this.$rules.start.push(rule); | |
this.$rules[blockRuleName].push(rule); | |
} | |
// Finally,setting up unpaired rules | |
for (let ruleName in rules.unpaired) { | |
const ruleRegex = rules.unpaired[ruleName].regex; | |
const rule = makeUnpairedRule(ruleName, ruleRegex); | |
this.$rules.start.push(rule); | |
this.$rules[blockRuleName].push(rule); | |
} | |
// Provide "defaultToken" rule with custom logic for blocks | |
this.$rules[blockRuleName].push({ | |
regex: ".", | |
onMatch: function(val, state, stack) { | |
if (!stack.length) { | |
stack.unshift(blockRuleName); | |
} | |
if (stack.skipNext) { | |
stack.skipNext = false; | |
} | |
this.next = blockRuleName; | |
return makeToken(blockRuleName, stack); | |
} | |
}); | |
} | |
this.$rules.start.push(defaultTokenRule); | |
// And create inner rules for "recursive" rules functionality | |
for (let ruleName in rules.paired) { | |
const ruleRegex = rules.paired[ruleName].regex; | |
this.$rules[ruleName] = makeInnerPairedRule(ruleName, ruleRegex); | |
} | |
this.normalizeRules(); | |
/** | |
* Create rule for block. Usually it must process first symbol of line | |
*/ | |
function makeBlockRule(name, regex) { | |
return { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
stack.unshift(name); | |
stack.skipNext = false; | |
return (this.next = name); | |
} | |
} | |
} | |
/** | |
* Create rule for paired "tags". For example: **bold**, _italic_, etc. | |
*/ | |
function makePairedRule(name, regex) { | |
return { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
if (stack.skipNext) { | |
stack.skipNext = false; | |
stack.unshift(state); | |
return makeToken((this.next = stack[0]), stack); | |
} | |
return makeToken((this.next = name), stack); | |
} | |
}; | |
} | |
/** | |
* Create rule for unpaired "tags". For example it may be url, email, date, etc. | |
*/ | |
function makeUnpairedRule(name, regex) { | |
return { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
stack.skipNext = false; | |
this.next = state; | |
return makeToken(`${name}.${state}`, stack); | |
} | |
}; | |
} | |
/** | |
* Create paired ace rule from token name and regex | |
*/ | |
function pairedRuleFactory(name, regex) { | |
return { | |
regex: regex, | |
makeRule: function(parent) { | |
return { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
if (stack.skipNext) { | |
stack.skipNext = false; | |
return makeToken((this.next = state), stack); | |
} | |
stack.unshift(parent); | |
return makeToken((this.next = name), stack); | |
} | |
}; | |
} | |
}; | |
} | |
/** | |
* Create unpaired ace rule from token name and regex | |
*/ | |
function unpairedRuleFactory(name, regex) { | |
return { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
stack.skipNext = false; | |
return makeToken((this.next = name), stack); | |
} | |
}; | |
} | |
/** | |
* Create inner rules. It provide ability to "recursive" rules | |
*/ | |
function makeInnerPairedRule(parentRuleName, regex) { | |
// Make behavior for inner rules | |
// This change stack state and make token for regexes | |
const innerRules = [startOfLineRule, backslashRule, { | |
regex: regex, | |
onMatch: function(val, state, stack) { | |
if (stack.skipNext) { | |
stack.skipNext = false; | |
return makeToken((this.next = state), stack); | |
} | |
stack.unshift(state); | |
const token = makeToken(state, stack); | |
stack.shift(); | |
this.next = stack.shift() || "start"; | |
return token; | |
} | |
}]; | |
// We must inject other rules to inner rule | |
for (let ruleName in rules.paired) { | |
if (ruleName !== parentRuleName) { | |
innerRules.push(rules.paired[ruleName].makeRule(parentRuleName)); | |
} | |
} | |
for (let ruleName in rules.unpaired) { | |
innerRules.push(makeUnpairedRule(ruleName, rules.unpaired[ruleName].regex)); | |
} | |
innerRules.push(defaultTokenRule); | |
return innerRules; | |
} | |
/** | |
* Create token from current state and stack of tokens | |
*/ | |
function makeToken(state, stack) { | |
if (stack.length) { | |
const tokens = { | |
[state]: true | |
}; | |
stack.forEach((token) => { | |
if (!tokens[token]) { | |
tokens[token] = true; | |
state += `.${token}`; | |
} | |
}); | |
} | |
return state; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment