Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
`@` stripped from attribute values in SSR mode
```html
<a href='mailto:hello@example.com'>email</a>
```

when run through the compiler in SSR mode produces

```javascript
...
return `<a href="mailto:helloexample.com">email</a>`.trim();
...
```
`@` stripped from attribute values in SSR mode
```html
<a href='mailto:hello@example.com'>email</a>
```

when run through the compiler in SSR mode produces

```javascript
...
return `<a href="mailto:helloexample.com">email</a>`.trim();
...
```


Discussion:
Looking at the code for this, I noticed something else: double quotes in attribute values in SSR are also not handled well -

```html
<span title='"foo"'>foo</span>
```

produces

```javascript
...
return `<span title=""foo"">foo</span>`.trim();
...
```
Looking at the code for this, I noticed something else: double quotes in attribute values in SSR are also not handled well -

```html
<span title='"foo"'>foo</span>
```

produces

```javascript
...
return `<span title=""foo"">foo</span>`.trim();
...
```

Resolving patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -4,7 +4,7 @@ import annotateWithScopes from '../../utils/annotateWithScopes';
 import isReference from '../../utils/isReference';
 import { walk } from 'estree-walker';
 import deindent from '../../utils/deindent';
-import stringify from '../../utils/stringify';
+import { stringify } from '../../utils/stringify';
 import CodeBuilder from '../../utils/CodeBuilder';
 import visit from './visit';
 import shared from './shared';
diff --git a/src/generators/dom/visitors/Component/Attribute.ts b/src/generators/dom/visitors/Component/Attribute.ts
--- a/src/generators/dom/visitors/Component/Attribute.ts
+++ b/src/generators/dom/visitors/Component/Attribute.ts
@@ -2,7 +2,7 @@ import { DomGenerator } from '../../index';
 import Block from '../../Block';
 import { Node } from '../../../../interfaces';
 import { State } from '../../interfaces';
-import stringify from '../../../../utils/stringify';
+import { stringify } from '../../../../utils/stringify';
 
 export default function visitAttribute(
 	generator: DomGenerator,
diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts
--- a/src/generators/dom/visitors/Element/Attribute.ts
+++ b/src/generators/dom/visitors/Element/Attribute.ts
@@ -1,6 +1,6 @@
 import attributeLookup from './lookup';
 import deindent from '../../../../utils/deindent';
-import stringify from '../../../../utils/stringify';
+import { stringify } from '../../../../utils/stringify';
 import getStaticAttributeValue from './getStaticAttributeValue';
 import { DomGenerator } from '../../index';
 import Block from '../../Block';
diff --git a/src/generators/dom/visitors/Text.ts b/src/generators/dom/visitors/Text.ts
--- a/src/generators/dom/visitors/Text.ts
+++ b/src/generators/dom/visitors/Text.ts
@@ -2,7 +2,7 @@ import { DomGenerator } from '../index';
 import Block from '../Block';
 import { Node } from '../../../interfaces';
 import { State } from '../interfaces';
-import stringify from '../../../utils/stringify';
+import { stringify } from '../../../utils/stringify';
 
 export default function visitText(
 	generator: DomGenerator,
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
--- a/src/generators/server-side-rendering/index.ts
+++ b/src/generators/server-side-rendering/index.ts
@@ -178,7 +178,12 @@ export default function ssr(
 		function __escape ( html ) {
 			return String( html ).replace( /["'&<>]/g, match => escaped[ match ] );
 		}
-	`.replace(/(\\)?@(\w*)/g, (match: string, escaped: string, name: string) => escaped ? match.slice(1) : generator.alias(name));
+	`.replace(/(\\)?([@#])(\w*)/g, (match: string, escaped: string, sigil: string, name: string) => {
+		if (escaped) return match.slice(1);
+		if (sigil !== '@') return match;
+
+		return generator.alias(name);
+	});
 
 	return generator.generate(result, options, { name, format });
 }
diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts
--- a/src/generators/server-side-rendering/visitors/Element.ts
+++ b/src/generators/server-side-rendering/visitors/Element.ts
@@ -4,6 +4,7 @@ import visit from '../visit';
 import visitWindow from './meta/Window';
 import { SsrGenerator } from '../index';
 import Block from '../Block';
+import { escape } from '../../../utils/stringify';
 import { Node } from '../../../interfaces';
 
 const meta = {
@@ -14,7 +15,7 @@ function stringifyAttributeValue(block: Block, chunks: Node[]) {
 	return chunks
 		.map((chunk: Node) => {
 			if (chunk.type === 'Text') {
-				return chunk.data;
+				return escape(chunk.data).replace(/"/g, '&quot;');
 			}
 
 			const { snippet } = block.contextualise(chunk.expression);
diff --git a/src/generators/server-side-rendering/visitors/Text.ts b/src/generators/server-side-rendering/visitors/Text.ts
--- a/src/generators/server-side-rendering/visitors/Text.ts
+++ b/src/generators/server-side-rendering/visitors/Text.ts
@@ -1,5 +1,6 @@
 import { SsrGenerator } from '../index';
 import Block from '../Block';
+import { escape } from '../../../utils/stringify';
 import { Node } from '../../../interfaces';
 
 export default function visitText(
@@ -7,5 +8,5 @@ export default function visitText(
 	block: Block,
 	node: Node
 ) {
-	generator.append(node.data.replace(/(\${|`|\\)/g, '\\$1').replace(/([^\\@#])?([@#])/g, '$1\\$2'));
+	generator.append(escape(node.data).replace(/(\${|`|\\)/g, '\\$1'));
 }
diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts
--- a/src/utils/stringify.ts
+++ b/src/utils/stringify.ts
@@ -1,3 +1,7 @@
-export default function stringify(data: string) {
-	return JSON.stringify(data.replace(/([^\\@#])?([@#])/g, '$1\\$2'));
+export function stringify(data: string) {
+	return JSON.stringify(escape(data));
+}
+
+export function escape(data: string) {
+	return data.replace(/([^\\@#])?([@#])/g, '$1\\$2');
 }


Test patch:
diff --git a/test/runtime/samples/attribute-static-at-symbol/_config.js b/test/runtime/samples/attribute-static-at-symbol/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-static-at-symbol/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `<a href='mailto:hello@example.com'>email</a>`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/attribute-static-at-symbol/main.html b/test/runtime/samples/attribute-static-at-symbol/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-static-at-symbol/main.html
@@ -0,0 +1 @@
+<a href='mailto:hello@example.com'>email</a>
\ No newline at end of file
diff --git a/test/runtime/samples/attribute-static-quotemarks/_config.js b/test/runtime/samples/attribute-static-quotemarks/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-static-quotemarks/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `<span title='"foo"'>foo</span>`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/attribute-static-quotemarks/main.html b/test/runtime/samples/attribute-static-quotemarks/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/attribute-static-quotemarks/main.html
@@ -0,0 +1 @@
+<span title='"foo"'>foo</span>
\ No newline at end of file