Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 113 additions & 5 deletions components/prism-markup.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,19 @@ Object.defineProperty(Prism.languages.markup.tag, 'addInlined', {
* @param {string} tagName The name of the tag that contains the inlined language. This name will be treated as
* case insensitive.
* @param {string} lang The language key.
* @param {InlineAttribute[]} [attributes] An optional record of attributes that have to have a certain value.
* @param {string | string[]} [before] An optional list of languages. This inline language will be checked before
* the given languages.
* @example
* addInlined('style', 'css');
* addInlined('script', 'none', [{ name: 'type': value: /text\/plain/ }]);
*
* @typedef InlineAttribute
* @property {RegExp} name
* @property {RegExp} value
* @property {boolean} [optional=false]
*/
value: function addInlined(tagName, lang) {
value: function addInlined(tagName, lang, attributes, before) {
var includedCdataInside = {};
includedCdataInside['language-' + lang] = {
pattern: /(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,
Expand All @@ -108,15 +117,112 @@ Object.defineProperty(Prism.languages.markup.tag, 'addInlined', {
inside: Prism.languages[lang]
};

var def = {};
def[tagName] = {
pattern: RegExp(/(<__[\s\S]*?>)(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?(?=<\/__>)/.source.replace(/__/g, function () { return tagName; }), 'i'),
/**
* Replaces all occurrences of `<KEY>` with the value of `replacements[KEY]`.
*
* @param {string} string
* @param {string[] | Object<string, string>} replacements
* @returns {string}
*/
function replace(string, replacements) {
return string.replace(/<(\w+)>/g, function (m, key) { return '(?:' + replacements[key] + ')'; });
}

// A general tag attribute.
var TAG_ATTR = /[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>]))/.source;
// The attribute loop of tags.
// This will consume at least one attribute
var TAG_ATTR_LOOP = replace(/(?:\s*<ATTR>)+/.source, { ATTR: TAG_ATTR });

// We assume that the opening tag can have any attributes by default. If there are some conditions, then the
// below code will just replace this value.
var openingTag = replace(/<<TAG>(?:\s<LOOP>)?\s*>/.source, { TAG: tagName, LOOP: TAG_ATTR_LOOP });
var closingTag = replace(/<\/<TAG>>/.source, { TAG: tagName });

// Now for the tricky part: attributes.
// We have to do two things: 1) guarantee that non-optional attributes are present and 2) of the attributes that
// are present, we have to guarantee that they have the correct value.
//
// Implementing 1) is relatively easy. After the tag name, we add a lookahead for each non-optional attribute
// checking that the attribute is present. (We do not check the value here.)
//
// Implementing 2) is harder. The basic loop that is used to consume markup attribute looks roughly like the
// following. In the pseudo pattern, AN is a general pattern of an attribute name and AV is a general attribute
// value:
// (\s+AN(=AV)?)*
// What we need here, is a conditional, so we can treat our target attributes differently and to do this, we use
// lookaheads again. In the following, AN<i> will i-th target attribute name and AV<i> will be the i-th
// attribute value:
// (\s+(AN1=AV1|AN2=AV2|...|(?!AN1|AN2|...)AN(=AV)?))*

if (attributes && attributes.length > 0) {

// Implement 1)
var required = '';
for (var attr, i = 0; attr = attributes[i++];) {
if (!attr.optional) {
required += replace(/(?=<LOOP>?\s*<N>\s*=)/.source, {
LOOP: TAG_ATTR_LOOP,
N: attr.name.source
});
}
}

// Implement 2)
var attrChoices = [
// the base case
replace(/(?!<NAMES>[\s/>=])<ATTR>/.source, {
ATTR: TAG_ATTR,
NAMES: attributes.map(function (a) { return a.name.source; }).join('|')
})
];
for (var attr, i = 0; attr = attributes[i++];) {
var choice = replace(
/<NAME>\s*=\s*(?:"(?=<VALUE>")[^"]*"|'(?=<VALUE>')[^']*'|(?=<VALUE>[\s>])[^\s'">=]+(?=[\s>]))/.source,
{ NAME: attr.name.source, VALUE: attr.value.source }
);
attrChoices.push(choice);
}
var attrLoop = replace(/(?:\s*<ATTR>)+/.source, { ATTR: attrChoices.join('|') });

// create the new opening tag pattern
if (required) {
// we require at least one attribute, so the loop isn't optional
openingTag = replace(/<<TAG>\s<REQ><LOOP>\s*>/.source, { TAG: tagName, REQ: required, LOOP: attrLoop });
} else {
// basically the same as the old one but with a different loop
openingTag = replace(/<<TAG>(?:\s<LOOP>)?\s*>/.source, { TAG: tagName, LOOP: attrLoop });
}
}

var pattern = '(' + openingTag + ')'
+ /(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?/.source
+ '(?=' + closingTag + ')';

var token = {
pattern: RegExp(pattern, 'i'),
lookbehind: true,
greedy: true,
inside: inside
};

Prism.languages.insertBefore('markup', 'cdata', def);
var beforeArray = before ? (Array.isArray(before) ? before : [before]) : [];
/** @type {Array} */
var existingTokens = Prism.languages.markup[tagName];
if (!existingTokens) {
var def = {};
def[tagName] = [token];
Prism.languages.insertBefore('markup', 'cdata', def);
} else {
var index = existingTokens.findIndex(function (t) {
return beforeArray.some(function (b) { return ('language-' + b) in t.inside; });
});
if (index === -1) {
existingTokens.push(token);
} else {
existingTokens.splice(index, 0, token);
}
}
}
});

Expand All @@ -128,3 +234,5 @@ Prism.languages.xml = Prism.languages.extend('markup', {});
Prism.languages.ssml = Prism.languages.xml;
Prism.languages.atom = Prism.languages.xml;
Prism.languages.rss = Prism.languages.xml;

Prism.languages.markup.tag.addInlined('script', 'none', [{ name: /type/, value: /text\/plain/ }]);
2 changes: 1 addition & 1 deletion components/prism-markup.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 113 additions & 5 deletions prism.js
Original file line number Diff line number Diff line change
Expand Up @@ -1291,10 +1291,19 @@ Object.defineProperty(Prism.languages.markup.tag, 'addInlined', {
* @param {string} tagName The name of the tag that contains the inlined language. This name will be treated as
* case insensitive.
* @param {string} lang The language key.
* @param {InlineAttribute[]} [attributes] An optional record of attributes that have to have a certain value.
* @param {string | string[]} [before] An optional list of languages. This inline language will be checked before
* the given languages.
* @example
* addInlined('style', 'css');
* addInlined('script', 'none', [{ name: 'type': value: /text\/plain/ }]);
*
* @typedef InlineAttribute
* @property {RegExp} name
* @property {RegExp} value
* @property {boolean} [optional=false]
*/
value: function addInlined(tagName, lang) {
value: function addInlined(tagName, lang, attributes, before) {
var includedCdataInside = {};
includedCdataInside['language-' + lang] = {
pattern: /(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,
Expand All @@ -1314,15 +1323,112 @@ Object.defineProperty(Prism.languages.markup.tag, 'addInlined', {
inside: Prism.languages[lang]
};

var def = {};
def[tagName] = {
pattern: RegExp(/(<__[\s\S]*?>)(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?(?=<\/__>)/.source.replace(/__/g, function () { return tagName; }), 'i'),
/**
* Replaces all occurrences of `<KEY>` with the value of `replacements[KEY]`.
*
* @param {string} string
* @param {string[] | Object<string, string>} replacements
* @returns {string}
*/
function replace(string, replacements) {
return string.replace(/<(\w+)>/g, function (m, key) { return '(?:' + replacements[key] + ')'; });
}

// A general tag attribute.
var TAG_ATTR = /[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>]))/.source;
// The attribute loop of tags.
// This will consume at least one attribute
var TAG_ATTR_LOOP = replace(/(?:\s*<ATTR>)+/.source, { ATTR: TAG_ATTR });

// We assume that the opening tag can have any attributes by default. If there are some conditions, then the
// below code will just replace this value.
var openingTag = replace(/<<TAG>(?:\s<LOOP>)?\s*>/.source, { TAG: tagName, LOOP: TAG_ATTR_LOOP });
var closingTag = replace(/<\/<TAG>>/.source, { TAG: tagName });

// Now for the tricky part: attributes.
// We have to do two things: 1) guarantee that non-optional attributes are present and 2) of the attributes that
// are present, we have to guarantee that they have the correct value.
//
// Implementing 1) is relatively easy. After the tag name, we add a lookahead for each non-optional attribute
// checking that the attribute is present. (We do not check the value here.)
//
// Implementing 2) is harder. The basic loop that is used to consume markup attribute looks roughly like the
// following. In the pseudo pattern, AN is a general pattern of an attribute name and AV is a general attribute
// value:
// (\s+AN(=AV)?)*
// What we need here, is a conditional, so we can treat our target attributes differently and to do this, we use
// lookaheads again. In the following, AN<i> will i-th target attribute name and AV<i> will be the i-th
// attribute value:
// (\s+(AN1=AV1|AN2=AV2|...|(?!AN1|AN2|...)AN(=AV)?))*

if (attributes && attributes.length > 0) {

// Implement 1)
var required = '';
for (var attr, i = 0; attr = attributes[i++];) {
if (!attr.optional) {
required += replace(/(?=<LOOP>?\s*<N>\s*=)/.source, {
LOOP: TAG_ATTR_LOOP,
N: attr.name.source
});
}
}

// Implement 2)
var attrChoices = [
// the base case
replace(/(?!<NAMES>[\s/>=])<ATTR>/.source, {
ATTR: TAG_ATTR,
NAMES: attributes.map(function (a) { return a.name.source; }).join('|')
})
];
for (var attr, i = 0; attr = attributes[i++];) {
var choice = replace(
/<NAME>\s*=\s*(?:"(?=<VALUE>")[^"]*"|'(?=<VALUE>')[^']*'|(?=<VALUE>[\s>])[^\s'">=]+(?=[\s>]))/.source,
{ NAME: attr.name.source, VALUE: attr.value.source }
);
attrChoices.push(choice);
}
var attrLoop = replace(/(?:\s*<ATTR>)+/.source, { ATTR: attrChoices.join('|') });

// create the new opening tag pattern
if (required) {
// we require at least one attribute, so the loop isn't optional
openingTag = replace(/<<TAG>\s<REQ><LOOP>\s*>/.source, { TAG: tagName, REQ: required, LOOP: attrLoop });
} else {
// basically the same as the old one but with a different loop
openingTag = replace(/<<TAG>(?:\s<LOOP>)?\s*>/.source, { TAG: tagName, LOOP: attrLoop });
}
}

var pattern = '(' + openingTag + ')'
+ /(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?/.source
+ '(?=' + closingTag + ')';

var token = {
pattern: RegExp(pattern, 'i'),
lookbehind: true,
greedy: true,
inside: inside
};

Prism.languages.insertBefore('markup', 'cdata', def);
var beforeArray = before ? (Array.isArray(before) ? before : [before]) : [];
/** @type {Array} */
var existingTokens = Prism.languages.markup[tagName];
if (!existingTokens) {
var def = {};
def[tagName] = [token];
Prism.languages.insertBefore('markup', 'cdata', def);
} else {
var index = existingTokens.findIndex(function (t) {
return beforeArray.some(function (b) { return ('language-' + b) in t.inside; });
});
if (index === -1) {
existingTokens.push(token);
} else {
existingTokens.splice(index, 0, token);
}
}
}
});

Expand All @@ -1335,6 +1441,8 @@ Prism.languages.ssml = Prism.languages.xml;
Prism.languages.atom = Prism.languages.xml;
Prism.languages.rss = Prism.languages.xml;

Prism.languages.markup.tag.addInlined('script', 'none', [{ name: /type/, value: /text\/plain/ }]);


/* **********************************************
Begin prism-css.js
Expand Down
41 changes: 39 additions & 2 deletions tests/languages/markup!+javascript/javascript_inclusion.test
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ let foo = '</script>';
"foo"
</script>

<!-- Don't highlight plain text -->
<script type="text/plain">
var foo = 0;
</script>

----------------------------------------------------

[
Expand All @@ -20,7 +25,9 @@ let foo = '</script>';
["punctuation", "<"],
"script"
]],
["attr-name", ["type"]],
["attr-name", [
"type"
]],
["attr-value", [
["punctuation", "="],
["punctuation", "\""],
Expand Down Expand Up @@ -69,7 +76,9 @@ let foo = '</script>';
["punctuation", "<"],
"script"
]],
["attr-name", ["type"]],
["attr-name", [
"type"
]],
["attr-value", [
["punctuation", "="],
["punctuation", "\""],
Expand Down Expand Up @@ -101,6 +110,34 @@ let foo = '</script>';
["string", "\"foo\""]
]]
]],
["tag", [
["tag", [
["punctuation", "</"],
"script"
]],
["punctuation", ">"]
]],

["comment", "<!-- Don't highlight plain text -->"],
["tag", [
["tag", [
["punctuation", "<"],
"script"
]],
["attr-name", [
"type"
]],
["attr-value", [
["punctuation", "="],
["punctuation", "\""],
"text/plain",
["punctuation", "\""]
]],
["punctuation", ">"]
]],
["script", [
["language-none", "\r\nvar foo = 0;\r\n"]
]],
["tag", [
["tag", [
["punctuation", "</"],
Expand Down