-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathedraw-dom-svg.el
4407 lines (3802 loc) · 173 KB
/
edraw-dom-svg.el
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; edraw-dom-svg.el --- DOM/SVG Utility -*- lexical-binding: t; -*-
;; Copyright (C) 2021 AKIYAMA Kouhei
;; Author: AKIYAMA Kouhei <misohena@gmail.com>
;; Keywords: Graphics,Drawing,SVG
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'dom)
(require 'seq)
(require 'subr-x)
(require 'edraw-math)
(require 'edraw-path)
(require 'edraw-util)
(defvar edraw-svg-version "1.1")
(defvar edraw-dom-inhibit-parent-links nil)
;;;; DOM Element Creation
(defun edraw-dom-element (tag &rest attr-plist-and-children)
"Return a new DOM element with TAG and ATTR-PLIST-AND-CHILDREN.
ATTR-PLIST-AND-CHILDREN specifies the attributes and children of
the new element. For example:
(edraw-dom-element
\\='div
:class \"some-div\"
(edraw-dom-element \\='p \"Paragraph 1.\")
(edraw-dom-element \\='p \"Paragraph 2.\"))
Attributes are specified in a property list starting at the
beginning of ATTR-PLIST-AND-CHILDREN. A property list key must be
a symbol (and non-nil). If the symbol is a keyword, the leading
colon is ignored (i.e. :x and \\='x are the same).
If a non-symbol (or nil) appears at the position where the key
symbol of the property list should appear, the subsequent
elements are treated as children. Children that are nil are
automatically removed.
The following special properties can be specified.
:parent Parent DOM element. Can be specified only once.
:children A list of child DOM nodes. Can be specified multiple.
:attributes A plist or alist of additional attributes. Can be specified
multiple.
Commonly used SVG elements have their own creation functions:
- `edraw-svg-create'
- `edraw-svg-rect'
- `edraw-svg-circle'
- `edraw-svg-ellipse'
- `edraw-svg-line'
- `edraw-svg-path'
- `edraw-svg-polygon'
- `edraw-svg-polyline'
- `edraw-svg-group'
These functions can specify the same arguments as this function
in the rest argument."
(let ((rest attr-plist-and-children)
parent
children
attr-alist)
;; Split ATTR-PLIST-AND-CHILDREN into ATTR-ALIST and CHILDREN.
(while (and rest
(car rest) ;; Nil means invalid child
(symbolp (car rest)))
(let* ((key (car rest))
(value (cadr rest))
(attr-name (edraw-dom-element--strip-colon key)))
(pcase attr-name
;; Support :parent <parent> notation.
('parent
(setq parent value))
;; Support :children (<child> ...) notation.
('children
(setq children (nconc children (delq nil (copy-sequence value)))))
;; Support :attributes (<key> <value> ...) or ((<key> . <value>) ...)
('attributes
(when (consp value)
(cond
((consp (car value)) ;; alist
(cl-loop for (k . v) in value
when (and k (symbolp k))
do (push (cons (edraw-dom-element--strip-colon k) v)
attr-alist)))
((edraw-plistp value) ;; plist
(cl-loop for (k v) on value by #'cddr
when (and k (symbolp k))
do (push (cons (edraw-dom-element--strip-colon k) v)
attr-alist))))))
(_
(push (cons attr-name value) attr-alist)))
(setq rest (cddr rest))))
(setq attr-alist (nreverse attr-alist))
(setq children (nconc children (delq nil (copy-sequence rest))))
;; Create an element
(let ((element (apply #'dom-node tag attr-alist children)))
;; Set ELEMENT as parent for children
(unless edraw-dom-inhibit-parent-links
(dolist (child children)
(edraw-dom-set-parent-auto child element)))
;; Append the element to parent
(when parent
(edraw-dom-append-child parent element))
element)))
;; TEST: (edraw-dom-element 'rect :x 1 :y 2 :width 3 :height 4) => (rect ((x . 1) (y . 2) (width . 3) (height . 4)))
;; TEST: (edraw-dom-element 'rect :x 1 :attributes '(:y 2 :width 3) :height 4) => (rect ((x . 1) (y . 2) (width . 3) (height . 4)))
;; TEST: (edraw-dom-element 'rect :x 1 :attributes '(:y 2 :width 3 nil 10) :attributes '((:height . 4) (nil . 11)) nil) => (rect ((x . 1) (y . 2) (width . 3) (height . 4)))
;; TEST: (let ((edraw-dom-inhibit-parent-links t)) (edraw-dom-element 'g :stroke "red" :children (list (edraw-dom-element 'rect :x 11 :y 22 :width 33 :height 44) nil) (edraw-dom-element 'rect :x 111 :y 222 :width 333 :height 444) nil)) => (g ((stroke . "red")) (rect ((x . 11) (y . 22) (width . 33) (height . 44))) (rect ((x . 111) (y . 222) (width . 333) (height . 444))))
;; TEST: (let ((edraw-dom-inhibit-parent-links t) (g (dom-node 'g))) (edraw-dom-element 'rect :parent g :x 11 :y 22 :width 33 :height 44) g) => (g nil (rect ((x . 11) (y . 22) (width . 33) (height . 44))))
(defun edraw-dom-element--strip-colon (key)
(cond
((keywordp key) (intern (substring (symbol-name key) 1)))
((symbolp key) key)))
(defun edraw-dom-copy-tree (node)
"Duplicate the DOM tree NODE.
Attribute keys and values, and text node strings are shared
before and after copying.
Each element in the cloned tree has no link to its parent
element. Call `edraw-dom-update-parent-links' explicitly if necessary.
Attributes for internal use are not duplicated.
Whether it is for internal use is determined by `edraw-dom-attr-internal-p'."
(if (and (consp node)
(symbolp (car node)))
(let* ((tag (dom-tag node))
(attributes (cl-loop for (key . value) in (dom-attributes node)
unless (edraw-dom-attr-internal-p key)
collect (cons key value)))
(children (cl-loop for child in (dom-children node)
collect (edraw-dom-copy-tree child))))
(apply #'dom-node tag attributes children)
;; Do not call `edraw-dom-set-parent' and
;; (edraw-dom-element tag :attributes attributes :children children)
)
node))
;;;; DOM Element Accessors
(defun edraw-dom-element-p (node)
(and node
(listp node)
(not (null (car node)))
(symbolp (car node))))
(defmacro edraw-dom-tag (node)
"Return the NODE tag.
Unlike `dom-tag', this function doesn't consider NODE if is's a
list of nodes.
Since this is a macro, setf can be used."
;; depends on dom.el node structure
`(car-safe ,node))
(defmacro edraw-dom-attributes (node)
"Return the NODE attribute list.
Unlike `dom-attributes', this function doesn't consider NODE if
is's a list of nodes.
Since this is a macro, setf can be used."
;; depends on dom.el node structure
`(cadr ,node))
(defmacro edraw-dom-children (node)
"Return the NODE child list.
Unlike `dom-children', this function doesn't consider NODE if
is's a list of nodes.
Since this is a macro, setf can be used."
;; depends on dom.el node structure
`(cddr ,node))
(defun edraw-dom-tag-eq (node tag)
(eq (edraw-dom-tag node) tag))
;;;; DOM Search
(defun edraw-dom-get-by-id (parent id)
(car (dom-by-id parent (concat "\\`" (regexp-quote id) "\\'"))))
(defun edraw-dom-get-or-create (parent tag id)
(or
(edraw-dom-get-by-id parent id)
(edraw-dom-element tag :id id :parent parent)))
;;;; DOM Comparison
;; Note: It is inappropriate to use `equal' to compare DOM nodes. In
;; particular, if there is an internal attribute such as a link to a
;; parent node, `equal' cannot be used to compare correctly. Also,
;; even though the contents of the attributes are the same, it is
;; possible that only the order of the attributes is different.
(defun edraw-dom-equal (node1 node2 &optional
attrs-to-exclude-for-top-nodes
attrs-to-exclude-for-children
without-children)
(if (edraw-dom-element-p node1)
(if (edraw-dom-element-p node2)
;; Element
(and (eq (edraw-dom-tag node1) (edraw-dom-tag node2))
(edraw-dom-equal-attributes (edraw-dom-attributes node1)
(edraw-dom-attributes node2)
attrs-to-exclude-for-top-nodes)
(or without-children
(edraw-dom-equal-children node1 node2
attrs-to-exclude-for-children)))
nil)
(if (edraw-dom-element-p node2)
nil
;; Text node
(equal node1 node2))))
(defun edraw-dom-equal-children (node1 node2 &optional attrs-to-exclude)
(when (and (edraw-dom-element-p node1) (edraw-dom-element-p node2))
(edraw-dom-equal-node-list (edraw-dom-children node1)
(edraw-dom-children node2)
attrs-to-exclude)))
(defun edraw-dom-equal-node-list (nodes1 nodes2 &optional attrs-to-exclude)
(when (= (length nodes1) (length nodes2))
(while (and nodes1
nodes2
(edraw-dom-equal (car nodes1) (car nodes2) attrs-to-exclude))
(setq nodes1 (cdr nodes1)
nodes2 (cdr nodes2)))
(and (null nodes1)
(null nodes2))))
(defun edraw-dom-equal-attributes (attrs1 attrs2 &optional attrs-to-exclude)
(seq-set-equal-p
(seq-remove (lambda (attr) (or (edraw-dom-attr-internal-p (car attr))
(memq (car attr) attrs-to-exclude)))
attrs1)
(seq-remove (lambda (attr) (or (edraw-dom-attr-internal-p (car attr))
(memq (car attr) attrs-to-exclude)))
attrs2)
#'equal))
;;;; DOM Parent Tracking
(defun edraw-dom-set-parent-auto (node parent)
(unless edraw-dom-inhibit-parent-links
(edraw-dom-set-parent node parent))
node)
(defun edraw-dom-set-parent (node parent)
(when (edraw-dom-element-p node)
;; :-edraw-dom-parent is an attribute for internal use.
;; (See: `edraw-dom-attr-internal-p')
(dom-set-attribute node :-edraw-dom-parent parent))
node)
(defun edraw-dom-get-parent (node)
(when (edraw-dom-element-p node)
(dom-attr node :-edraw-dom-parent)))
(defun edraw-dom-reset-parent (node)
(when (edraw-dom-element-p node)
(edraw-dom-remove-attr node :-edraw-dom-parent)))
(defun edraw-dom-update-parent-links (tree)
"Make it possible to retrieve parents of all elements in TREE."
(when (edraw-dom-element-p tree)
(dolist (child (dom-children tree))
(edraw-dom-set-parent child tree)
(edraw-dom-update-parent-links child))))
(defun edraw-dom-remove-parent-links (tree)
"Remove links to parent from all nodes in TREE."
(edraw-dom-reset-parent tree)
(when (edraw-dom-element-p tree)
(dolist (child (dom-children tree))
(edraw-dom-remove-parent-links child))))
(defun edraw-dom-get-root (node)
(let (parent)
(while (setq parent (edraw-dom-get-parent node))
(setq node parent))
node))
(defun edraw-dom-get-ancestor-by-tag (node tag)
(let (parent)
(while (and (setq parent (edraw-dom-get-parent node))
(not (eq (dom-tag parent) tag)))
(setq node parent))
parent))
(defun edraw-dom-parent (dom node)
"Return the parent of NODE in DOM.
Same as `dom-parent', but if NODE has the parent node information
set by `dom-set-parent', this function will skip searching from
the DOM and quickly identify the parent."
(let ((parent (edraw-dom-get-parent node)))
(if (and parent (memq node (edraw-dom-children parent)))
parent
(dom-parent dom node))))
;;;; DOM Removing
(defun edraw-dom-remove-node (dom node)
(prog1 (dom-remove-node dom node)
;; @todo Should check to see if it has really been removed.
(edraw-dom-reset-parent node)))
(defun edraw-dom-remove-all-children (node)
(when (consp node)
(dolist (child (dom-children node))
(edraw-dom-reset-parent child))
(setf (edraw-dom-children node) nil))
node)
(defun edraw-dom-remove-by-id (dom id)
(when-let* ((node (edraw-dom-get-by-id dom id)))
(edraw-dom-remove-node dom node)))
(defun edraw-dom-remove-attr (node attr)
(dom-set-attributes node (assq-delete-all attr (dom-attributes node))))
(defun edraw-dom-remove-attr-if (node pred)
(dom-set-attributes node (cl-delete-if pred (dom-attributes node))))
(defun edraw-dom-remove-attr-from-tree (tree attr)
(edraw-dom-do tree
(lambda (node _ancestors)
(when (edraw-dom-element-p node)
(edraw-dom-remove-attr node attr))))
tree)
;;;; DOM Insertion
(defun edraw-dom-add-child-before (node child &optional before)
(prog1 (dom-add-child-before node child before)
(edraw-dom-set-parent-auto child node)))
(defun edraw-dom-append-child (node child)
(prog1 (dom-append-child node child)
(edraw-dom-set-parent-auto child node)))
(defun edraw-dom-append-children (node children)
(dolist (child children)
(edraw-dom-append-child node child)))
(defun edraw-dom-insert-first (node child)
(prog1 (dom-add-child-before node child)
(edraw-dom-set-parent-auto child node)))
(defun edraw-dom-insert-nth (node child index)
(setq node (dom-ensure-node node))
;; depends on dom.el node structure
(if (<= index 0)
(setcdr (cdr node) (cons child (cddr node)))
(let ((cell (or (nthcdr (1- index) (cddr node))
(last (cddr node)))))
(setcdr cell (cons child (cdr cell)))))
(edraw-dom-set-parent-auto child node)
child)
;;;; DOM Retrieve Siblings
(defun edraw-dom-first-child (node)
(car (dom-children node)))
(defun edraw-dom-last-child (node)
(car (last (dom-children node))))
(defun edraw-dom-next-sibling (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(let ((siblings (dom-children parent)))
(while (and siblings
(not (eq (car siblings) node)))
(setq siblings (cdr siblings)))
(cadr siblings))))
(defun edraw-dom-previous-sibling (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(let ((siblings (dom-children parent)))
(if (eq (car siblings) node)
nil
(while (and (cadr siblings)
(not (eq (cadr siblings) node)))
(setq siblings (cdr siblings)))
(car siblings)))))
;;;; DOM Ordering
(defun edraw-dom-first-node-p (dom node)
(if-let* ((parent (edraw-dom-parent dom node)))
(eq (car (dom-children parent)) node)
t))
(defun edraw-dom-last-node-p (dom node)
(if-let* ((parent (edraw-dom-parent dom node)))
(eq (car (last (dom-children parent))) node)
t))
(defun edraw-dom-reorder-prev (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(let ((index (seq-position (dom-children parent) node #'eq)))
(when (> index 0)
(let* ((prev-cell (nthcdr (1- index) (dom-children parent)))
(prev-node (car prev-cell)))
;; depends on dom.el node structure
(setcar prev-cell node)
(setcar (cdr prev-cell) prev-node))
t))))
(defun edraw-dom-reorder-next (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(let* ((index (seq-position (dom-children parent) node #'eq))
(curr-cell (nthcdr index (dom-children parent)))
(next-cell (cdr curr-cell))
(next-node (car next-cell)))
(when next-cell
;; depends on dom.el node structure
(setcar next-cell node)
(setcar curr-cell next-node)
t))))
(defun edraw-dom-reorder-first (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(when (not (eq (car (dom-children parent)) node))
;; The parent of NODE does not change.
(dom-remove-node parent node)
(dom-add-child-before parent node (car (dom-children parent)))
t)))
(defun edraw-dom-reorder-last (dom node)
(when-let* ((parent (edraw-dom-parent dom node)))
(when (not (eq (car (last (dom-children parent))) node))
;; The parent of NODE does not change.
(dom-remove-node parent node)
(dom-append-child parent node)
t)))
;;;; DOM Attributes
(defun edraw-dom-attr-internal-p (attr-name)
"Return non-nil if the attribute's name ATTR-NAME is for internal use.
ATTR-NAME is a symbol or string.
Attribute names starting with a colon are for internal use."
(cond
((symbolp attr-name) (keywordp attr-name))
((stringp attr-name) (and (not (string-empty-p attr-name))
(eq (aref attr-name 0) ?:)))))
(defun edraw-dom-remove-internal-attributes (node)
(when (edraw-dom-element-p node)
(edraw-dom-remove-attr-if node
(lambda (attr)
(edraw-dom-attr-internal-p (car attr)))))
node)
(defun edraw-dom-remove-internal-attributes-from-tree (node)
(edraw-dom-do
node
(lambda (node _ancestors)
(edraw-dom-remove-internal-attributes node)))
node)
(defun edraw-dom-set-attribute-name (node old-name new-name)
"Rename OLD-NAME attribute in NODE to NEW-NAME if it exists.
If the attribute named OLD-NAME does not exist, do nothing.
Attribute value is preserved."
(setq node (dom-ensure-node node))
(let* ((attributes (cadr node))
(old-cell (assoc old-name attributes)))
(when old-cell
(setcar old-cell new-name))))
;;;; DOM Attribute Inheritance
(defun edraw-dom-attr-with-inherit (element attr)
(let (value)
(while (and element
(null (setq value (dom-attr element attr))))
(setq element (edraw-dom-get-parent element)))
value))
;; TEST: (let ((group (edraw-svg-group :stroke "black" (edraw-svg-path "M0 0 100 100")))) (edraw-dom-attr-with-inherit (car (edraw-dom-children group)) 'stroke)) => "black"
;; TEST: (let ((group (edraw-svg-group (edraw-svg-path "M0 0 100 100")))) (edraw-dom-attr-with-inherit (car (edraw-dom-children group)) 'stroke)) => nil
;;;; DOM Mapping
(defun edraw-dom-do (node function &optional ancestors)
(funcall function node ancestors)
(when (edraw-dom-element-p node)
(let ((ancestors (cons node ancestors))
(children (dom-children node)))
(cond
((listp children)
(dolist (child-node children)
(edraw-dom-do child-node function ancestors)))
;; ;; Comment Node (comment nil "comment text")
;; ;; @todo Isn't it unnecessary?
;; ((stringp children)
;; (funcall function children ancestors))
))))
;;;; DOM Top Level Handling
(defun edraw-dom-split-top-nodes (dom)
"Split DOM into pre comment nodes, top-level element, and post
comment nodes.
Return (ROOT-ELEMENT . (PRE-COMMENTS . POST-COMMENTS)).
`libxml-parse-xml-region' returns an element with the tag top if
there are comments before or after root element. This function
splits the DOM into pre comment nodes, root element, and post
comment nodes."
(if (edraw-dom-tag-eq dom 'top)
;; DOM contains comments directly below
(let* ((top-nodes (dom-children dom))
(p top-nodes)
(pre-comments nil))
(while (and p (edraw-dom-tag-eq (car p) 'comment))
(push (car p) pre-comments)
(setq p (cdr p)))
(if p
;; (ROOT-ELEMENT . (PRE-COMMENTS . POST-COMMENTS))
(cons (car p) (cons (nreverse pre-comments) (cdr p)))
;; No elements!
(cons nil (cons top-nodes nil))))
(cons dom nil)))
(defun edraw-dom-merge-top-nodes (root-element pre-comments post-comments)
"Reverse operation of `edraw-dom-split-top-nodes'."
;;@todo If (edraw-dom-tag-eq root-element 'top)?
(if (or pre-comments post-comments)
(apply #'dom-node 'top nil
(append pre-comments (list root-element) post-comments))
root-element))
;;;; CSS
;;;;; Regexp
;; https://www.w3.org/TR/css-syntax-3/#token-diagrams
;; https://www.w3.org/TR/CSS21/grammar.html
(defconst edraw-css-re-comment "\\(?:/\\*.*?\\*/\\)") ;; non-greedy
(defconst edraw-css-re-newline "\\(?:\r\n\\|[\n\r\f]\\)")
(defconst edraw-css-re-ws "\\(?:\r\n\\|[\n\r\f \t]\\)")
(defconst edraw-css-re-ws? (concat edraw-css-re-ws "?"))
(defconst edraw-css-re-ws* (concat edraw-css-re-ws "*"))
(defconst edraw-css-re-escape (concat
"\\(?:" "\\\\"
"\\(?:" "[^\n\r\f[:xdigit:]]" "\\|"
"[[:xdigit:]]\\{1,6\\}" edraw-css-re-ws?
"\\)" "\\)"))
(defconst edraw-css-re-nmstart (concat
"\\(?:[_a-zA-Z]\\|[[:nonascii:]]\\|"
edraw-css-re-escape "\\)"))
(defconst edraw-css-re-nmchar (concat
"\\(?:[-_a-zA-Z0-9]\\|[[:nonascii:]]\\|"
edraw-css-re-escape "\\)"))
(defconst edraw-css-re-ident (concat
"\\(?:--\\|-?" edraw-css-re-nmstart "\\)"
edraw-css-re-nmchar "*"))
(defconst edraw-css-re-function (concat edraw-css-re-ident "("))
(defconst edraw-css-re-at-keyword (concat "@" edraw-css-re-ident))
(defconst edraw-css-re-hash (concat "#" edraw-css-re-nmchar "+"))
(defconst edraw-css-re-string-escape (concat
"\\(?:"
edraw-css-re-escape "\\|"
"\\\\" edraw-css-re-newline
"\\)"))
(defconst edraw-css-re-string1 (concat
"\"" "\\(?:[^\n\r\f\"\\\\]\\|"
edraw-css-re-string-escape "\\)*" "\""))
(defconst edraw-css-re-string2 (concat
"'" "\\(?:[^\n\r\f'\\\\]\\|"
edraw-css-re-string-escape "\\)*" "'"))
(defconst edraw-css-re-string (concat "\\(?:" edraw-css-re-string1 "\\|"
edraw-css-re-string2 "\\)"))
(defconst edraw-css-re-url-arg (concat
"\\(?:"
"[!#$%&*-~]\\|[[:nonascii:]]\\|"
edraw-css-re-escape
"\\)*"))
(defconst edraw-css-re-url-rest (concat
edraw-css-re-ws*
"\\("
edraw-css-re-url-arg
"\\)"
edraw-css-re-ws*
")"))
(defconst edraw-css-re-number ;;edraw-svg-re-number
(concat "\\(?:"
"[-+]?"
;; Valid: 12 12.34 .34 Invalid: 12.
"\\(?:[0-9]+\\(?:\\.[0-9]+\\)?\\|\\.[0-9]+\\)"
"\\(?:[eE][-+]?[0-9]+\\)?"
"\\)"))
(defconst edraw-css-re-dimension (concat
edraw-css-re-number
edraw-css-re-ident))
(defconst edraw-css-re-percentage (concat edraw-css-re-number "%"))
;; @todo Support bad-*, unicode-range token
;; Remaining tokens:
;; bad-string
;; bad-url
;; delim
;; unicode-range
;; CDO, CDC
;; colon
;; semicolon
;; comma
;; [, ]
;; (, )
;; {, }
;;;;; Unescape
(defun edraw-css-unescape (string)
(replace-regexp-in-string
edraw-css-re-string-escape
(lambda (text)
(let ((ch (aref text 1)))
(cond
((or (<= ?0 ch ?9) (<= ?a ch ?f) (<= ?A ch ?F))
(let ((cp (string-to-number (substring text 1) 16)))
;;@todo check range
(char-to-string cp)))
((memq ch '(?\r ?\n ?\f)) "")
(t (substring text 1)))))
string t t))
;; TEST: (edraw-css-unescape "u\\rl(") => "url("
;; TEST: (edraw-css-unescape "u\\72 l(") => "url("
;; TEST: (edraw-css-unescape "line1\\\r\nline1\\\nline1") => "line1line1line1"
;; TEST: (edraw-css-unescape "\\26 B") => "&B"
;; TEST: (edraw-css-unescape "\\000026B") => "&B"
;;;;; Escape
(defun edraw-css-escape (string)
(replace-regexp-in-string "[\\\\\n\r\f\"]"
(lambda (str) (format "\\%X " (aref str 0)))
string t t))
;; TEST: (edraw-css-escape "backslash:\\ doublequote:\" CR:\r") => "backslash:\\5C doublequote:\\22 CR:\\D "
;;;;; Tokenize
(defconst edraw-css-re-token
(concat
edraw-css-re-comment "*"
"\\(?:\\(" edraw-css-re-ws "\\)"
"\\|\\(" edraw-css-re-string "\\)" ;; " '
"\\|\\(" edraw-css-re-hash "\\)" ;; #
"\\|\\(" edraw-css-re-at-keyword "\\)" ;; @
"\\|\\(" edraw-css-re-dimension "\\)"
"\\|\\(" edraw-css-re-percentage "\\)"
"\\|\\(" edraw-css-re-number "\\)"
"\\|\\(" edraw-css-re-function "\\)"
"\\|\\(" edraw-css-re-ident "\\)"
"\\|\\(" "[]({}),:;[]" "\\)"
"\\|\\(" "." "\\)" ;; ( { [ ] } ) , : ; delim
"\\|\\(" "\\'" "\\)"
"\\)"))
(defun edraw-css-match (regexp str ppos &optional noerror)
(let ((pos (car ppos)))
(if (equal (string-match regexp str pos) pos)
(setcar ppos (match-end 0))
(unless noerror
(error "CSS Syntax Error: %s `%s'" pos str)))))
(defun edraw-css-token (str ppos)
(edraw-css-match edraw-css-re-token str ppos)
(let* ((index (cl-loop for index from 1
;; match-data is 100x slower than match-beginning
when (match-beginning index)
return index))
(range (cons (match-beginning index) (match-end index))))
(pcase index
(1 (cons 'ws range))
(2 (cons 'string range))
(3 (cons 'hash range))
(4 (cons 'at-keyword range))
(5 (cons 'dimension range))
(6 (cons 'percentage range))
(7 (cons 'number range))
(8
;; URL or Function
(let ((fname (edraw-css-unescape
(substring str (car range) (1- (cdr range))))))
(if (string= (downcase fname) "url")
(progn
(edraw-css-match edraw-css-re-url-rest str ppos)
(let ((url-end (match-end 0)))
(cons 'url (cons (car range) url-end))))
(cons 'function range))))
(9 (cons 'ident range))
(10
;; ( { [ ] } ) , : ;
(cons (intern (substring str (car range) (cdr range))) range))
(11
(let ((delim-char (aref str (car range))))
(when (or (= delim-char ?\") (= delim-char ?\'))
(error "Bad string at %s" (car range))))
(cons 'delim range))
(12 (cons 'EOF range)))))
(defun edraw-css-token-string (str token)
(let ((beg (cadr token))
(end (cddr token)))
(substring str beg end)))
(defun edraw-css-token-value (str token)
(let ((type (car token))
(beg (cadr token))
(end (cddr token)))
(pcase type
('ws (substring str beg end))
('string (edraw-css-unescape (substring str (1+ beg) (1- end))))
('hash (edraw-css-unescape (substring str (1+ beg) end)))
('at-keyword (edraw-css-unescape (substring str (1+ beg) end)))
('dimension (substring str beg end)) ;;@todo to number and unit?
('percentage (substring str beg end)) ;;@todo to number and unit?
('number (substring str beg end)) ;;@todo to number?
('url
(string-match (concat "(" edraw-css-re-url-rest) str beg)
(match-string 1 str))
('function (edraw-css-unescape (substring str beg (1- end))))
('ident (edraw-css-unescape (substring str beg end)))
('delim (aref str beg))
('EOF nil)
;; ( { [ ] } ) , : ;
(_ type))))
(defun edraw-css-token-test (str pos)
(let* ((ppos (list pos))
(result (edraw-css-token str ppos)))
(list (car result)
(cdr result)
(car ppos)
(edraw-css-token-value str result))))
;; TEST: (edraw-css-token-test " hoge" 0) => (ws (0 . 1) 1 " ")
;; TEST: (edraw-css-token-test "/* hoge */hoge" 0) => (ident (10 . 14) 14 "hoge")
;; TEST: (edraw-css-token-test "'hoge\"ho\\ge\"'" 0) => (string (0 . 13) 13 "hoge\"hoge\"")
;; TEST: (edraw-css-token-test "'hoge\"ho\\ge\"" 0) => error
;; TEST: (edraw-css-token-test "\"hoge\nhoge\"" 0) => error
;; TEST: (edraw-css-token-test "\'hoge\nhoge\'" 0) => error
;; TEST: (edraw-css-token-test "@hoge" 0) => (at-keyword (0 . 5) 5 "hoge")
;; TEST: (edraw-css-token-test " #hoge" 1) => (hash (1 . 6) 6 "hoge")
;; TEST: (edraw-css-token-test " 100px" 1) => (dimension (1 . 6) 6 "100px")
;; TEST: (edraw-css-token-test " 100%" 1) => (percentage (1 . 5) 5 "100%")
;; TEST: (edraw-css-token-test " 100 " 1) => (number (1 . 4) 4 "100")
;; TEST: (edraw-css-token-test " 100. " 1) => (number (1 . 4) 4 "100")
;; TEST: (edraw-css-token-test " fun(hoge) " 1) => (function (1 . 5) 5 "fun")
;; TEST: (edraw-css-token-test " u\\rl( https://misohena.jp/?q=hoge ) " 1) => (url (1 . 36) 36 "https://misohena.jp/?q=hoge")
;; TEST: (edraw-css-token-test " url( https://misohena.jp/" 1) => error
;; TEST: (edraw-css-token-test "hoge" 0) => (ident (0 . 4) 4 "hoge")
;; TEST: (edraw-css-token-test " { " 2) => ({ (2 . 3) 3 {)
;; TEST: (edraw-css-token-test " ! " 2) => (delim (2 . 3) 3 33)
;;;;; Parse
(defun edraw-css-skip-ws* (str ppos)
(edraw-css-match edraw-css-re-ws* str ppos))
(defun edraw-css-expect (str ppos type)
(let ((beg (car ppos))
(token (edraw-css-token str ppos)))
(unless (eq (car token) type)
(error "Unexpected token: `%s' %s `%s'" (car token) beg str))
(cdr token)))
(defun edraw-css-skip-simple-block (str ppos start-token)
;; https://www.w3.org/TR/css-syntax-3/#consume-simple-block
(let ((ending-token (pcase (car start-token)
('\[ '\])
('\{ '\})
;; '\( or 'function
(_ '\)))))
(while (let ((cvtt (edraw-css-skip-component-value str ppos)))
(when (eq cvtt 'EOF) (error "Unexpected token: `%s'" cvtt))
(not (eq cvtt ending-token))))
'simple-block))
(defun edraw-css-skip-component-value (str ppos &optional token)
;; https://www.w3.org/TR/css-syntax-3/#component-value-diagram
;; https://www.w3.org/TR/css-syntax-3/#parse-component-value
;; https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
(let ((token (or token
(progn
(edraw-css-skip-ws* str ppos)
(edraw-css-token str ppos)))))
(pcase (car token)
((or '\( '\{ '\[ 'function)
(edraw-css-skip-simple-block str ppos token)
;; Return 'simple-block
)
(type type))))
(defun edraw-css-skip-at-rule (str ppos)
;; https://www.w3.org/TR/css-syntax-3/#consume-at-rule
(while (progn
(edraw-css-skip-ws* str ppos)
(let ((token (edraw-css-token str ppos)))
(pcase (car token)
('ws t) ;; This is not used because skip-ws* is called, but just in case
('\; nil)
('EOF
(error "Unexpected %s in at-rule `%s'" token str)
nil)
('\{
(edraw-css-skip-simple-block str ppos token)
nil)
(_
(edraw-css-skip-component-value str ppos token)
t))))))
(defun edraw-css-split-decl-list (str ppos)
;; https://www.w3.org/TR/css-syntax-3/#consume-list-of-declarations
(let (decls)
(while (progn
(edraw-css-skip-ws* str ppos)
(let ((token-beg (car ppos))
(token (edraw-css-token str ppos)))
(pcase (car token)
('ws t) ;; This is not used because skip-ws* is called, but just in case
(': t)
('EOF nil)
('at-keyword
(edraw-css-skip-at-rule str ppos)
t)
('ident
(edraw-css-skip-ws* str ppos)
(edraw-css-expect str ppos ':)
(edraw-css-skip-ws* str ppos)
(let ((bov (car ppos)) ;;@todo include comments?
eov)
(while (progn
(pcase (edraw-css-skip-component-value str ppos)
('\; (setq eov (1- (car ppos))) nil)
('EOF (setq eov (car ppos)) nil)
(_ t))))
(push (cons token (cons bov eov)) decls))
t)
(_ (error "Unexpected token: `%s' %s `%s'" token str token-beg) nil)))))
(nreverse decls)))
;; TEST: (edraw-css-split-decl-list "margin :0 auto " (list 0)) => (((ident 0 . 6) 8 . 16))
;; TEST: (edraw-css-split-decl-list "margin" (list 0)) => error
;; TEST: (edraw-css-split-decl-list "prop1: fun(hoge" (list 0)) => error
;; TEST: (edraw-css-split-decl-list "123: 456" (list 0)) => error
;; TEST: (edraw-css-split-decl-list "@unsupported { splines: reticulating } color: green" (list 0)) => (((ident 39 . 44) 46 . 51)) Example From: https://drafts.csswg.org/css-style-attr/#style-attribute:~:text=%3Cspan-,style%3D%22%40unsupported,-%7B%20splines%3A%20reticulating%20%7D%20color
;; TEST: (edraw-css-split-decl-list "@unsupported opt1 \"hoge\" 123; color: green" (list 0)) => (((ident 30 . 35) 37 . 42))
(defun edraw-css-split-decl-list-as-strings (str ppos)
(mapcar (lambda (prop)
(cons (edraw-css-token-value str (car prop))
(substring str (cadr prop) (cddr prop))))
(edraw-css-split-decl-list str ppos)))
;; TEST: (edraw-css-split-decl-list-as-strings "margin :0 auto " (list 0)) => (("margin" . "0 auto "))
;; TEST: (edraw-css-split-decl-list-as-strings "font-size:14px;fill:red;" (list 0)) => (("font-size" . "14px") ("fill" . "red"))
;; TEST: (edraw-css-split-decl-list-as-strings "font-size:14px;font-family : \"Helvetica Neue\", \"Arial\", sans-serif;" (list 0)) => (("font-size" . "14px") ("font-family" . "\"Helvetica Neue\", \"Arial\", sans-serif"))
;; TEST: (edraw-css-split-decl-list-as-strings "prop1: func( a b ; c d ); prop2: { aa bb ; cc dd}" (list 0)) => (("prop1" . "func( a b ; c d )") ("prop2" . "{ aa bb ; cc dd}"))
;; TEST: (edraw-css-split-decl-list-as-strings "str\\oke: u\\72 l(https://misohena.jp/blog/?q=;)" (list 0)) => (("stroke" . "u\\72 l(https://misohena.jp/blog/?q=;)"))
;; TEST: (edraw-css-split-decl-list-as-strings "a:1;b:2;c3;c4" (list 0)) => error
;;;;; Convert Value
(defun edraw-css-value-to-lisp-value (css-value &optional element attr)
(let ((ppos (list 0))
token)
;; Get the first non-whitespace token
(while (eq (car (setq token (edraw-css-token css-value ppos))) 'ws))
(pcase (car token)
((or 'number 'dimension 'percentage)
(edraw-svg-attr-length-to-number
(edraw-css-token-value css-value token) element attr))
('string
(edraw-css-token-value css-value token))
((or 'hash 'ident)
;; color?
;; hash => #ff00ff
;; ident => black
(edraw-css-token-value css-value token))
((or 'hash 'function 'ident)
;; color?
;; function => rgb(255,255,255)
;; Return css-value as is
;; @todo unescape all tokens
css-value)
(_ ;;(or 'at-keyword 'url 'delim '\( '\{ '\[ '\] '\} '\) '\, '\: '\;)
nil))))
;;;;; Convert Alist and Plist
(defun edraw-css-decl-list-to-alist (str &optional ppos)
(unless ppos (setq ppos (list 0)))
(let ((alist (ignore-errors ;;@todo Recover error (Skip to next `;')
(edraw-css-split-decl-list-as-strings str ppos))))
(dolist (prop alist)
(setcar prop (intern (car prop))))
alist))
;; TEST: (edraw-css-decl-list-to-alist "font-size:14px;fill:red;") => ((font-size . "14px") (fill . "red"))
(defun edraw-css-decl-list-to-plist (str &optional ppos)
(edraw-n-alist-to-plist (edraw-css-decl-list-to-alist str ppos)))
;; TEST: (edraw-css-decl-list-to-plist "font-size:14px;fill:red;") => (font-size "14px" fill "red")
;;;;; Check CSS String
(defun edraw-css-component-value-list-p (str)
"Return non-nil if STR is a list of component values."
(ignore-errors
(let ((ppos (list 0)))
(edraw-css-skip-ws* str ppos)
(let (eov)
(while (progn
(pcase (edraw-css-skip-component-value str ppos)
('\; (setq eov (1- (car ppos))) nil)
('EOF (setq eov (car ppos)) nil)
(_ t))))
(= eov (length str))))))
;; (edraw-css-component-value-list-p "") => t
;; (edraw-css-component-value-list-p "123") => t
;; (edraw-css-component-value-list-p "123 \"abc\" 234") => t
;; (edraw-css-component-value-list-p "123 \"abc 234") => nil
;; (edraw-css-component-value-list-p "value 1 2 3") => t
;; (edraw-css-component-value-list-p "func(1,2,3)") => t
;; (edraw-css-component-value-list-p "a(b") => nil
;; (edraw-css-component-value-list-p "a; b") => nil
;;;;; Make CSS String
(defun edraw-css-make-ident (string)
"Create an ident-token string with the name specified by STRING.
If STRING contains characters that cannot be used as an ident-token,
they will be escaped.
If STRING cannot be converted to an ident-token, an error will be
signaled. This includes the empty string and \"-\"."
(cond
((or (equal string "") (equal string "-"))
(error "Too short ident-token (%s)" string))
((string-match "\\`\\(?:-?\\([^-_a-zA-Z\u0080-\U0010ffff]\\)\\)" string)
(let ((ep (match-beginning 1)))
(concat
(substring string 0 ep)
(format "\\%X " (aref string ep))
(replace-regexp-in-string
"[^-a-zA-Z0-9_\u0080-\U0010ffff]"
(lambda (str) (format "\\%X " (aref str 0)))
string t t nil (1+ ep)))))
(t
(replace-regexp-in-string
"[^-a-zA-Z0-9_\u0080-\U0010ffff]"
(lambda (str) (format "\\%X " (aref str 0)))
string t t))))
;; TEST: (edraw-css-make-ident "") => error
;; TEST: (edraw-css-make-ident "-") => error
;; TEST: (edraw-css-make-ident "a") => "a"
;; TEST: (edraw-css-make-ident "--") => "--"
;; TEST: (edraw-css-make-ident "font-weight") => "font-weight"
;; TEST: (edraw-css-make-ident "-font-weight") => "-font-weight"
;; TEST: (edraw-css-make-ident "--font-weight") => "--font-weight"
;; TEST: (edraw-css-make-ident "100-%") => "\\31 00-\\25 "
;; TEST: (edraw-css-make-ident "-100-%") => "-\\31 00-\\25 "
(defun edraw-css-make-decl (name value)
"Create a string that represents a declaration with NAME and VALUE.
NAME is a string or symbol that has not yet been escaped.