修 Pencil 在 expoert web 的 bug, 順便學了 XML,XSLT,XPath

  1. 修 Pencil 在 expoert web 的 bug, 順便學了 XML,XSLT,XPath
    1. 使用情境
    2. 問題描述
    3. 探查問題原因
    4. 不懂的地方
    5. 解決不會的地方
      1. 什麼是 xslt 檔案
      2. 蓋一個 hello world
    6. XSLT 的基本語法
      1. tab name, attributes 上 : 的寫法,是什麼意思?
      2. attributes 裡的 value 有一些看不懂的這些記號
      3. What is XPath?
    7. 解決方案

修 Pencil 在 expoert web 的 bug, 順便學了 XML,XSLT,XPath

repo: https://github.com/evolus/pencil

使用情境

做了兩頁面

  • index 點擊 button 可以連到 detail
  • detail 點擊 button 可以連到 index

想要匯出

問題描述

做好了的頁面,可以點擊區域與畫面沒有吻合。

「點擊區」如圖藍色透明區域
「畫面」如綠色透明區域

點擊 back to index 目前是無法跑到另一頁的,因為可點擊範圍 (map > area) 的縮放放與圖片 (img) 的縮放比例不一樣。

在此的點擊範圍實作方式,是利用 HTML <map> Tag 技術

匯出來的 html

html

<div class="Page" id="index_page">
<h2>index</h2>
<div class="ImageContainer">
<img src="pages/index.png" width="NaN" height="NaN" usemap="#map_index">
</div>
<map name="map_index">
<area shape="rect" coords="50,91,142,116" href="#detail_page" title="Go to page 'detail'">
</map>
</div>

探查問題原因

回過頭找找看 source code 怎麼會發生這個問題。

app/pencil-core/templates/HTML/default.HTML/StyleSheet.xslt

<div class="ImageContainer">
<img src="{@rasterized}"
width="{p:Properties/p:Property[@name='width']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
height="{p:Properties/p:Property[@name='height']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
usemap="#map_{p:Properties/p:Property[@name='fid']/text()}"/>
</div>
<map name="map_{p:Properties/p:Property[@name='fid']/text()}">
<xsl:apply-templates select="p:Links/p:Link" />
</map>

找到了一段,匯出 web page 時,定義圖片 width/height 的地方。
而匯出結果的 html 檔案看起來是兩個東西在 JS 裡運算,出問題產生 NaN,看來這個運算,是相乘?

驗證

將原始碼中的

  • "{p:Properties/p:Property[@name='width']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"

分母刪除,改成

  • "{p:Properties/p:Property[@name='width']/text()}"

先讓疑似相乘造成 NaN 驗證一次,確認了問題發生的地方,再來深入研究。

再次匯出,圖片的比例就比一開始的圖片小許多了。

<div class="Page" id="index_page">
<h2>index</h2>
<div class="ImageContainer">
<img src="pages/index.png" width="192" height="166" usemap="#map_index">
</div>
<map name="map_index">
<area shape="rect" coords="50,91,142,116" href="#detail_page" title="Go to page 'detail'">
</map>
</div>

匯出的圖片寬高也正常許多。

到此,是我在 2020 年提的 issue Export single web page width/height is NaN #604

並且同年提了 PR fix-export-single-web-page-width-height-is-NaN-for-development #610 但是遲遲沒有合併,因為它是 workaround 吧?

沒有研究其它的地方就提的 PR。

不懂的地方

最不懂的是這個檔案 app/pencil-core/templates/HTML/default.HTML/StyleSheet.xslt

首先,什麼是 .xslt 檔?

xslt 檔中,看起來是寫 xml 但是它的 xml 在 tab name 和 attributes 上有 : 的寫法,不知道有什麼意思

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:p="http://www.evolus.vn/Namespace/Pencil"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.w3.org/1999/xhtml">
<xsl:output method="html"/>
<xsl:template match="p:Page">
<xsl:if test="p:Note">
<xsl:apply-templates select="p:Note/node()" mode="processing-notes"/>

在 attributes 裡的 value,也有一些看不懂的 {}, \, p:, [@name=""], text() 的這些記號

<img src="{@rasterized}"
width="{p:Properties/p:Property[@name='width']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
height="{p:Properties/p:Property[@name='height']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
usemap="#map_{p:Properties/p:Property[@name='fid']/text()}"
/>

整理一下問題

  1. 什麼是 xslt 檔?
  2. tab name 上 : 的寫法,是什麼意思?
  3. attributes 上 : 的寫法,是什麼意思?
  4. attributes 裡的 value,也有一些看不懂的這些記號
    • {}
    • \
    • p:
    • [@name=""]
    • text()

解決不會的地方

什麼是 xslt 檔案

With XSLT you can transform an XML document into HTML.
[name=https://www.w3schools.com/xml/xml_xslt.asp]

用來轉換 XML 成 HTML!(這不就是我想要的功能?繼續看看)

看了一下 w3schools 的介紹,似乎不能直接把檔案文進瀏覽器,而它必須要經過一個程式轉換才可以看見結果。

蓋一個 hello world

我需要 xslt 的 hello world 示範。

這個影片,示範了使用 vscode 的環境,將 xslt 跑起來,並成功轉換 xml 檔案,我也跟著介紹。

安裝外掛 XSLT/XPath for Visual Studio Code

說明文件中有 Running XSLT 的介紹,看 Saxon-JS setup 的做法

安裝

npm init -y
npm install --save-dev xslt3

使用

  1. cmd+shift+pTasks: Run Build Task
  2. 選生成一個 Saxon-JS 的設定

並且修改如下

  • ${workspaceFolder}: 目前的專案根目錄

因為只是 hello world 所以我設定成下面的檔名,方便執行。

  • index.xsl
  • index.xml
  • index.html
{
"version": "2.0.0",
"tasks": [
{
"type": "xslt-js",
"label": "xslt-js: Saxon-JS Transform (New)",
"xsltFile": "${workspaceFolder}/index.xsl",
"xmlSource": "${workspaceFolder}/index.xml",
"resultPath": "${workspaceFolder}/index.xml",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$saxon-xslt-js"
]
}
]
}

成功轉換之後,就可以繼續看一下 w3schoolds 裡其它的介紹,看看 XML 還有什麼功能是關於這個問題中的疑問吧!

XSLT 的基本語法

  • XSLT <template>
  • XSLT <value-of>
  • XSLT <for-each>
  • XSLT <sort>
  • XSLT <if>
  • XSLT <choose>
<xsl:template match="p:Page"></xsl:template>
<xsl:value-of select="catalog/cd/title"/>
<xsl:for-each select="catalog/cd"></xsl:for-each>
<xsl:sort select="artist"/>
<xsl:if test="p:Note"></xsl:if>
<xsl:choose>
<xsl:when test="expression">
... some output ...
</xsl:when>
<xsl:otherwise>
... some output ....
</xsl:otherwise>
</xsl:choose>

tab name, attributes 上 : 的寫法,是什麼意思?

XML 有一個 namespace 的寫法。

XML Namespaces provide a method to avoid element name conflicts.
[name=https://www.w3schools.com/xml/xml_namespaces.asp]

利用不同的 namespace 可以使用重複的命名 (不過就是一個前綴)

<h:table>
<h:tr>
<h:td>Apples</h:td>
<h:td>Bananas</h:td>
</h:tr>
</h:table>

<f:table>
<f:name>African Coffee Table</f:name>
<f:width>80</f:width>
<f:length>120</f:length>
</f:table>

而且可以在 root 節點定義

定義兩個 namespace,hf 並且用兩個 URI 來定義。而這個 URI 在 XML 並不會當作 link 而只是當作一個「字串」而已。

<root
xmlns:h="http://www.w3.org/TR/html4/"
xmlns:f="https://www.w3schools.com/furniture"
></root>

attributes 裡的 value 有一些看不懂的這些記號

大概就是使用 XPath 相關的東西。那 XPath 概念上是什麼呢?

看一下 XPath Example

What is XPath?

XPath is a major element in the XSLT standard.

XPath can be used to navigate through elements and attributes in an XML document.

在 XSLT 中抓 XML 文件裡的元素,使用的表達方式。

拿個例子來 demo

XML file

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<cd>
<title>Empire Burlesque</title>
<artist>Bob Dylan</artist>
<country>USA</country>
<company>Columbia</company>
<price>10.90</price>
<year>1985</year>
</cd>
<cd>
<title>Hide your heart</title>
<artist>Bonnie Tyler</artist>
<country>UK</country>
<company>CBS Records</company>
<price>9.90</price>
<year>1988</year>
</cd>
<cd>
<title>Greatest Hits</title>
<artist>Dolly Parton</artist>
<country>USA</country>
<company>RCA</company>
<price>9.90</price>
<year>1982</year>
</cd>
</catalog>

XLST file

<xsl:for-each select="catalog/cd">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="artist"/></td>
</tr>
</xsl:for-each>

在 XLST 中,有一段 select="catalog/cd" 就是樹狀結構 catalog > cd 裡面有 titleartist 這種表示法就是 XPath

解決方案

重新定義問題:
目前的問題是 XLST 轉換 XML 時出現的問題。

目前只知道,下面這段原始碼的 p:Properties/p:Property[@name='bitmapScale']/text() 可能有問題 (因為刪掉它就沒問題了,但刪掉它不能真正解決問題)。

width="{
p:Properties/p:Property[@name='width']/text() *
p:Properties/p:Property[@name='bitmapScale']/text()
}"
height="{
p:Properties/p:Property[@name='height']/text() *
p:Properties/p:Property[@name='bitmapScale']/text()
}"

但還需要 XML 檔案,才可以轉換。
在這個時候,我就想到了,用除錯模式啟動程式碼看看。

就試看看指令

$ npm run start:dev   

並且操作一樣的匯成 web page 的動作

看見 devTool 的 console 出現了一行

xmlFile:/var/folders/80/ljnyxvfd22b86h1slh6187pr0000gn/T/tmp-131911KeuYqT5JSHb.xml

把它打開

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<Document xmlns="http://www.evolus.vn/Namespace/Pencil">
<Properties>
<Property name="activeId">b826fd07699d4543a02b200d8db96726</Property>
<Property name="exportTime">Fri Apr 08 2022 19:56:58 GMT+0800 (台北標準時間)</Property>
<Property name="exportTimeShort">202248</Property>
<Property name="fileName">backend</Property>
<Property name="friendlyName">backend</Property>
<Property name="bitmapScale">1</Property>
</Properties>
<Pages>
<Page xmlns:p="http://www.evolus.vn/Namespace/Pencil" id="02e736b4249f4f0b8f322fc9a45093e1" rasterized="/Users/chris/Desktop/pencil file/pages/index.png">
<Properties>
<Property name="id">02e736b4249f4f0b8f322fc9a45093e1</Property>
<Property name="fid">index</Property>
<Property name="name">index</Property>
<Property name="width">192</Property>
<Property name="height">166</Property>
<Property name="pageFileName">page_02e736b4249f4f0b8f322fc9a45093e1.xml</Property>
<Property name="zoom">1</Property>
<Property name="backgroundColorRGBA">rgba(255, 255, 255, 0)</Property>
</Properties>
<Links>
<Link target="b826fd07699d4543a02b200d8db96726" targetName="detail" targetFid="detail" x="50" y="91" w="92" h="25" />
</Links>
</Page>
<Page xmlns:p="http://www.evolus.vn/Namespace/Pencil" id="b826fd07699d4543a02b200d8db96726" rasterized="/Users/chris/Desktop/pencil file/pages/detail.png">
<Properties>
<Property name="id">b826fd07699d4543a02b200d8db96726</Property>
<Property name="fid">detail</Property>
<Property name="name">detail</Property>
<Property name="width">201</Property>
<Property name="height">293</Property>
<Property name="pageFileName">page_b826fd07699d4543a02b200d8db96726.xml</Property>
<Property name="zoom">1</Property>
<Property name="backgroundColorRGBA">rgba(255, 255, 255, 0)</Property>
</Properties>
<Links>
<Link target="02e736b4249f4f0b8f322fc9a45093e1" targetName="index" targetFid="index" x="50" y="113.25" w="92" h="25.75" />
</Links>
</Page>
</Pages>
</Document>

觀察發現

  • Document\Properties\PropertybitmapScale 但沒有 width, height
  • Document\Pages\PageProperties\Propertywidth, height 但沒有 bitmapScale

再看一次 XLST

<xsl:template match="p:Page">
...
<img src="{@rasterized}"
width="{p:Properties/p:Property[@name='width']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
height="{p:Properties/p:Property[@name='height']/text() * p:Properties/p:Property[@name='bitmapScale']/text()}"
usemap="#map_{p:Properties/p:Property[@name='fid']/text()}"/>
...
</xsl:template>

p: 是 namespace 先不管。
<xsl:template>match 指定 XPath 為 Page
<img>heightwidth 指定 Properties\Property

屬性名稱 = width 的要和屬性名稱 = bitmapScale 相乘,幾乎等同於 [@name='width'] * [@name='bitmapScale'] 這樣。

那是不是因為 Document\Pages\PageProperties\Property 沒有 bitmapScale 造成抓到的值是 undefined 所以相乘成為 NaN

心想: 那我來補一個 1 給他吧!

所以,就修改了程式,並且提交了新的 Pull Request #713

app/pencil-core/exporter/documentExportManager.js

351
352
353
354
355
var propertyNode = pageNode.ownerDocument.createElementNS(PencilNamespaces.p, "p:Property");
Dom.getSingle("./p:Properties", pageNode).appendChild(propertyNode);

propertyNode.setAttribute("name", "bitmapScale");
propertyNode.appendChild(dom.createTextNode(1));