Skip to content

Instantly share code, notes, and snippets.

@geakstr
Last active March 4, 2016 15:11
Show Gist options
  • Save geakstr/68cc92493feb166271ff to your computer and use it in GitHub Desktop.
Save geakstr/68cc92493feb166271ff to your computer and use it in GitHub Desktop.
Create "recursive" rules for ace editor
/**
* 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