summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/static_site_editor/services/templater.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/static_site_editor/services/templater.js')
-rw-r--r--app/assets/javascripts/static_site_editor/services/templater.js89
1 files changed, 89 insertions, 0 deletions
diff --git a/app/assets/javascripts/static_site_editor/services/templater.js b/app/assets/javascripts/static_site_editor/services/templater.js
new file mode 100644
index 00000000000..a1c1bb6b8d6
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/services/templater.js
@@ -0,0 +1,89 @@
+/**
+ * The purpose of this file is to modify Markdown source such that templated code (embedded ruby currently) can be temporarily wrapped and unwrapped in codeblocks:
+ * 1. `wrap()`: temporarily wrap in codeblocks (useful for a WYSIWYG editing experience)
+ * 2. `unwrap()`: undo the temporarily wrapped codeblocks (useful for Markdown editing experience and saving edits)
+ *
+ * Without this `templater`, the templated code is otherwise interpreted as Markdown content resulting in loss of spacing, indentation, escape characters, etc.
+ *
+ */
+
+const ticks = '```';
+const marker = 'sse';
+const wrapPrefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
+const wrapPostfix = `\n${ticks}`;
+const markPrefix = `${marker}-${Date.now()}`;
+
+const reHelpers = {
+ template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
+ openTag: '<[a-zA-Z]+.*?>',
+ closeTag: '</.+>',
+};
+const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
+const rePreexistingCodeBlocks = new RegExp(`(^${ticks}.*\\n(.|\\s)+?${ticks}$)`, 'gm');
+const reHtmlMarkup = new RegExp(
+ `^((${reHelpers.openTag}){1}(${reHelpers.template})*(${reHelpers.closeTag}){1})$`,
+ 'gm',
+);
+const reEmbeddedRubyBlock = new RegExp(`(^<%(${reHelpers.template})+%>$)`, 'gm');
+const reEmbeddedRubyInline = new RegExp(`(^.*[<|&lt;]%(${reHelpers.template})+$)`, 'gm');
+
+const patternGroups = {
+ ignore: [rePreexistingCodeBlocks],
+ // Order is intentional (general to specific) where HTML markup is marked first, then ERB blocks, then inline ERB
+ // Order in combo with the `mark()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
+ allow: [reHtmlMarkup, reEmbeddedRubyBlock, reEmbeddedRubyInline],
+};
+
+const mark = (source, groups) => {
+ let text = source;
+ let id = 0;
+ const hash = {};
+
+ Object.entries(groups).forEach(([groupKey, group]) => {
+ group.forEach(pattern => {
+ const matches = text.match(pattern);
+ if (matches) {
+ matches.forEach(match => {
+ const key = `${markPrefix}-${groupKey}-${id}`;
+ text = text.replace(match, key);
+ hash[key] = match;
+ id += 1;
+ });
+ }
+ });
+ });
+
+ return { text, hash };
+};
+
+const unmark = (text, hash) => {
+ let source = text;
+
+ Object.entries(hash).forEach(([key, value]) => {
+ const newVal = key.includes('ignore') ? value : `${wrapPrefix}${value}${wrapPostfix}`;
+ source = source.replace(key, newVal);
+ });
+
+ return source;
+};
+
+const unwrap = source => {
+ let text = source;
+ const matches = text.match(reTemplated);
+
+ if (matches) {
+ matches.forEach(match => {
+ const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, '');
+ text = text.replace(match, initial);
+ });
+ }
+
+ return text;
+};
+
+const wrap = source => {
+ const { text, hash } = mark(unwrap(source), patternGroups);
+ return unmark(text, hash);
+};
+
+export default { wrap, unwrap };