diff --git a/compiler/generate/visitors/RawMustacheTag.js b/compiler/generate/visitors/RawMustacheTag.js
new file mode 100644
index 0000000000..1b0b39e302
--- /dev/null
+++ b/compiler/generate/visitors/RawMustacheTag.js
@@ -0,0 +1,47 @@
+import deindent from '../utils/deindent.js';
+
+export default {
+ enter ( generator, node ) {
+ const name = generator.current.counter( 'raw' );
+
+ generator.addSourcemapLocations( node.expression );
+ const { snippet } = generator.contextualise( node.expression );
+
+ // we would have used comments here, but the `insertAdjacentHTML` api only
+ // exists for `Element`s.
+ const before = `${name}_before`;
+ generator.addElement( before, `document.createElement( 'noscript' )`, true );
+ const after = `${name}_after`;
+ generator.addElement( after, `document.createElement( 'noscript' )`, true );
+
+ const isToplevel = generator.current.localElementDepth === 0;
+
+ const mountStatement = deindent`
+ ${before}.insertAdjacentHTML( 'afterend', ${snippet} );
+ `;
+ const detachStatement = deindent`
+ while ( ${before}.nextSibling && ${before}.nextSibling !== ${after} ) {
+ ${before}.parentNode.removeChild( ${before}.nextSibling );
+ }
+ `;
+
+ if ( isToplevel ) {
+ generator.current.mountStatements.push(mountStatement);
+ } else {
+ generator.current.initStatements.push(mountStatement);
+ }
+
+ generator.current.updateStatements.push( deindent`
+ ${detachStatement}
+ ${mountStatement}
+ ` );
+
+ if ( isToplevel ) {
+ const { detachStatements } = generator.current;
+ // we need `before` and `after` to still be in the DOM when running the
+ // detach code, so splice in the detach code *before* detaching
+ // `before`/`after`.
+ detachStatements.splice( detachStatements.length - 2, 0, detachStatement);
+ }
+ }
+};
diff --git a/compiler/generate/visitors/index.js b/compiler/generate/visitors/index.js
index 70ba9e3298..8e125e5d8b 100644
--- a/compiler/generate/visitors/index.js
+++ b/compiler/generate/visitors/index.js
@@ -3,6 +3,7 @@ import EachBlock from './EachBlock.js';
import Element from './Element.js';
import IfBlock from './IfBlock.js';
import MustacheTag from './MustacheTag.js';
+import RawMustacheTag from './RawMustacheTag.js';
import Text from './Text.js';
import YieldTag from './YieldTag.js';
@@ -12,6 +13,7 @@ export default {
Element,
IfBlock,
MustacheTag,
+ RawMustacheTag,
Text,
YieldTag
};
diff --git a/compiler/parse/state/mustache.js b/compiler/parse/state/mustache.js
index b27e694608..1cc4532efd 100644
--- a/compiler/parse/state/mustache.js
+++ b/compiler/parse/state/mustache.js
@@ -190,6 +190,21 @@ export default function mustache ( parser ) {
});
}
+ // {{{raw}}} mustache
+ else if ( parser.eat( '{' ) ) {
+ const expression = readExpression( parser );
+
+ parser.allowWhitespace();
+ parser.eat( '}}}', true );
+
+ parser.current().children.push({
+ start,
+ end: parser.index,
+ type: 'RawMustacheTag',
+ expression
+ });
+ }
+
else {
const expression = readExpression( parser );
diff --git a/test/compiler/raw-mustaches/_config.js b/test/compiler/raw-mustaches/_config.js
new file mode 100644
index 0000000000..850a169e02
--- /dev/null
+++ b/test/compiler/raw-mustaches/_config.js
@@ -0,0 +1,16 @@
+const ns = '';
+export default {
+ data: {
+ raw: 'raw html!!!\\o/'
+ },
+ html: `before${ns}raw html!!!\\o/${ns}after`,
+
+ test ( assert, component, target ) {
+ component.set({ raw: '' });
+ assert.equal( target.innerHTML, `before${ns}${ns}after` );
+ component.set({ raw: 'how about unclosed elements?' });
+ assert.equal( target.innerHTML, `before${ns}how about unclosed elements?${ns}after` );
+ component.teardown();
+ assert.equal( target.innerHTML, '' );
+ }
+};
diff --git a/test/compiler/raw-mustaches/main.html b/test/compiler/raw-mustaches/main.html
new file mode 100644
index 0000000000..1a733843df
--- /dev/null
+++ b/test/compiler/raw-mustaches/main.html
@@ -0,0 +1 @@
+before{{{raw}}}after
diff --git a/test/parser/raw-mustaches/input.html b/test/parser/raw-mustaches/input.html
new file mode 100644
index 0000000000..349fc29186
--- /dev/null
+++ b/test/parser/raw-mustaches/input.html
@@ -0,0 +1 @@
+ {{{raw1}}} {{{raw2}}}
diff --git a/test/parser/raw-mustaches/output.json b/test/parser/raw-mustaches/output.json
new file mode 100644
index 0000000000..4acedf34ea
--- /dev/null
+++ b/test/parser/raw-mustaches/output.json
@@ -0,0 +1,48 @@
+{
+ "html": {
+ "start": 0,
+ "end": 30,
+ "type": "Fragment",
+ "children": [
+ {
+ "start": 0,
+ "end": 30,
+ "type": "Element",
+ "name": "p",
+ "attributes": [],
+ "children": [
+ {
+ "start": 4,
+ "end": 14,
+ "type": "RawMustacheTag",
+ "expression": {
+ "start": 7,
+ "end": 11,
+ "type": "Identifier",
+ "name": "raw1"
+ }
+ },
+ {
+ "start": 14,
+ "end": 15,
+ "type": "Text",
+ "data": " "
+ },
+ {
+ "start": 15,
+ "end": 25,
+ "type": "RawMustacheTag",
+ "expression": {
+ "start": 18,
+ "end": 22,
+ "type": "Identifier",
+ "name": "raw2"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "css": null,
+ "js": null
+}