From 30ce973b3ddf510c2f390542cc54ab6d7e6879c7 Mon Sep 17 00:00:00 2001
From: MoyuScript <i@moyu.moe>
Date: Sat, 5 Aug 2017 23:34:12 +0800
Subject: [PATCH] Implement textShadow rendering (Fix #499 and #908)

---
 src/CanvasRenderer.js        | 19 +++++++++++++++-
 src/NodeContainer.js         |  4 ++++
 src/parsing/textShadow.js    | 42 ++++++++++++++++++++++++++++++++++++
 tests/cases/text/shadow.html | 14 ++++++++++++
 4 files changed, 78 insertions(+), 1 deletion(-)
 create mode 100644 src/parsing/textShadow.js

diff --git a/src/CanvasRenderer.js b/src/CanvasRenderer.js
index f0af78f..029f3d0 100644
--- a/src/CanvasRenderer.js
+++ b/src/CanvasRenderer.js
@@ -6,6 +6,7 @@ import type Size from './drawing/Size';
 
 import type {BackgroundImage} from './parsing/background';
 import type {Border, BorderSide} from './parsing/border';
+import type {TextShadow} from './parsing/textShadow';
 
 import type {Path, BoundCurves} from './Bounds';
 import type {ImageStore, ImageElement} from './ImageLoader';
@@ -163,7 +164,23 @@ export default class CanvasRenderer {
     renderText(text: TextBounds, textContainer: TextContainer) {
         const container = textContainer.parent;
         this.ctx.fillStyle = container.style.color.toString();
-        this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
+        if (container.style.textShadow && text.text.trim().length) {
+            container.style.textShadow.slice(0).reverse().forEach(textShadow => {
+                this.ctx.shadowColor = textShadow.color.toString();
+                this.ctx.shadowOffsetX = textShadow.offsetX * this.options.scale;
+                this.ctx.shadowOffsetY = textShadow.offsetY * this.options.scale;
+                this.ctx.shadowBlur = textShadow.blur;
+
+                this.ctx.fillText(
+                    text.text,
+                    text.bounds.left,
+                    text.bounds.top + text.bounds.height
+                );
+            });
+        } else {
+            this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
+        }
+
         const textDecoration = container.style.textDecoration;
         if (textDecoration) {
             textDecoration.textDecorationLine.forEach(textDecorationLine => {
diff --git a/src/NodeContainer.js b/src/NodeContainer.js
index 218b052..cf62e59 100644
--- a/src/NodeContainer.js
+++ b/src/NodeContainer.js
@@ -10,6 +10,7 @@ import type {Font} from './parsing/font';
 import type {Overflow} from './parsing/overflow';
 import type {Padding} from './parsing/padding';
 import type {Position} from './parsing/position';
+import type {TextShadow} from './parsing/textShadow';
 import type {TextTransform} from './parsing/textTransform';
 import type {TextDecoration} from './parsing/textDecoration';
 import type {Transform} from './parsing/transform';
@@ -35,6 +36,7 @@ import {parseOverflow, OVERFLOW} from './parsing/overflow';
 import {parsePadding} from './parsing/padding';
 import {parsePosition, POSITION} from './parsing/position';
 import {parseTextDecoration} from './parsing/textDecoration';
+import {parseTextShadow} from './parsing/textShadow';
 import {parseTextTransform} from './parsing/textTransform';
 import {parseTransform} from './parsing/transform';
 import {parseVisibility, VISIBILITY} from './parsing/visibility';
@@ -63,6 +65,7 @@ type StyleDeclaration = {
     padding: Padding,
     position: Position,
     textDecoration: TextDecoration,
+    textShadow: Array<TextShadow> | null,
     textTransform: TextTransform,
     transform: Transform,
     visibility: Visibility,
@@ -106,6 +109,7 @@ export default class NodeContainer {
             padding: parsePadding(style),
             position: parsePosition(style.position),
             textDecoration: parseTextDecoration(style),
+            textShadow: parseTextShadow(style.textShadow),
             textTransform: parseTextTransform(style.textTransform),
             transform: parseTransform(style),
             visibility: parseVisibility(style.visibility),
diff --git a/src/parsing/textShadow.js b/src/parsing/textShadow.js
new file mode 100644
index 0000000..bc2b056
--- /dev/null
+++ b/src/parsing/textShadow.js
@@ -0,0 +1,42 @@
+/* @flow */
+'use strict';
+
+import Color from '../Color';
+
+export type TextShadow = {
+    color: Color,
+    offsetX: number,
+    offsetY: number,
+    blur: number
+};
+
+const TEXT_SHADOW_PROPERTY = /((rgba|rgb)\([^\)]+\)(\s-?\d+px){3})/g;
+const TEXT_SHADOW_VALUES = /(-?\d+px)|(#.+)|(rgb\(.+\))|(rgba\(.+\))/g;
+
+export const parseTextShadow = (textShadow: string): Array<TextShadow> | null => {
+    if (textShadow === 'none') {
+        return null;
+    }
+
+    const shadows = textShadow.match(TEXT_SHADOW_PROPERTY);
+
+    if (!shadows) {
+        return null;
+    }
+
+    const shadowList = [];
+
+    for (let i = 0; i < shadows.length; i++) {
+        const shadow = shadows[i].match(TEXT_SHADOW_VALUES);
+        if (shadow) {
+            shadowList.push({
+                color: new Color(shadow[0]),
+                offsetX: shadow[1] ? parseFloat(shadow[1].replace('px', '')) : 0,
+                offsetY: shadow[2] ? parseFloat(shadow[2].replace('px', '')) : 0,
+                blur: shadow[3] ? parseFloat(shadow[3].replace('px', '')) : 0
+            });
+        }
+    }
+
+    return shadowList;
+};
diff --git a/tests/cases/text/shadow.html b/tests/cases/text/shadow.html
index f353bbf..9dd72a0 100644
--- a/tests/cases/text/shadow.html
+++ b/tests/cases/text/shadow.html
@@ -27,6 +27,14 @@
             text-decoration: underline;
         }
 
+        .white-text-with-blue-shadow {
+            text-shadow: 1px 1px 2px black, 0 0 1em blue, 0 0 0.2em blue;
+            color: white;
+            font: 1.5em Georgia, serif;
+        }
+        .red-text-shadow {
+            text-shadow: 0 -2px;
+        }
     </style>
 
 </head>
@@ -39,5 +47,11 @@
     <span>testing with transparent</span>
     <strong>testing with low opacity</strong>
 </div>
+<p class="white-text-with-blue-shadow">Sed ut perspiciatis unde omnis iste
+    natus error sit voluptatem accusantium doloremque laudantium,
+    totam rem aperiam, eaque ipsa quae ab illo inventore.</p>
+<p class="red-text-shadow">Sed ut perspiciatis unde omnis iste
+    natus error sit voluptatem accusantium doloremque laudantium,
+    totam rem aperiam, eaque ipsa quae ab illo inventore.</p>
 </body>
 </html>