Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33b8cc3755 | ||
|
|
4383bc5633 | ||
|
|
a5ba058d24 | ||
|
|
ecaf47d8e6 | ||
|
|
3a41117ea0 | ||
|
|
76e4aaf49a | ||
|
|
239899da8d | ||
|
|
b175b06f31 | ||
|
|
5289b89ffc | ||
|
|
bf5d73de01 | ||
|
|
7d93711f9f | ||
|
|
054bd085ab | ||
|
|
3c04a4ce15 | ||
|
|
2f875b3bcb | ||
|
|
914f91f9f7 | ||
|
|
fcc6651b6a | ||
|
|
dda612c15e | ||
|
|
894a69d41f | ||
|
|
e2523f1d99 | ||
|
|
6fa35ae35c | ||
|
|
d0a2d31b5a | ||
|
|
43838e1ccc | ||
|
|
741e7902db | ||
|
|
6426d318fb | ||
|
|
8a98b4ee40 | ||
|
|
dcaac482de | ||
|
|
ee27df7680 | ||
|
|
e0ae29d64a | ||
|
|
db78e66451 | ||
|
|
b8ecd58005 | ||
|
|
2725834c4a | ||
|
|
42cb2d5d88 | ||
|
|
3994429b88 | ||
|
|
237bb1e0d9 | ||
|
|
911aad42cc | ||
|
|
36ded28ce4 | ||
|
|
9761bb91e9 | ||
|
|
435a310590 | ||
|
|
d377b3dbca | ||
|
|
850fe285b4 | ||
|
|
2adeb8dbcb | ||
|
|
fb5dfc4399 | ||
|
|
29e1cff8f1 | ||
|
|
2c3373ae16 | ||
|
|
c294ad0d4d | ||
|
|
ee436cc7cc | ||
|
|
168995493f | ||
|
|
e477f8b3da | ||
|
|
516329a408 | ||
|
|
938b7353ff | ||
|
|
a80721b2d3 | ||
|
|
4e065b9a80 | ||
|
|
aa8d7d02d1 | ||
|
|
05b36fc58f | ||
|
|
d6196ecad8 | ||
|
|
21a91421a6 | ||
|
|
272f51ad5b | ||
|
|
c352591d72 | ||
|
|
96caad46a5 | ||
|
|
309cae316b | ||
|
|
ffd0c75e8b | ||
|
|
fdf313472c | ||
|
|
011fdba7d3 | ||
|
|
e5e8521ae3 | ||
|
|
786e752417 | ||
|
|
d2220c68bd | ||
|
|
26d40e1149 | ||
|
|
15a55e6e07 | ||
|
|
32ab8d6573 | ||
|
|
ec55ae8b00 | ||
|
|
689adc0a30 | ||
|
|
fab9d7c0ab | ||
|
|
4aec2aa6da | ||
|
|
f46372b97c | ||
|
|
33c661505e | ||
|
|
91e16f7863 | ||
|
|
55e8c331a6 | ||
|
|
ae092cd1a7 | ||
|
|
122dc2ec0f | ||
|
|
c25b918a51 | ||
|
|
173bc97d0f | ||
|
|
84166ab954 | ||
|
|
a1cfd1de25 | ||
|
|
58fcae7af8 | ||
|
|
29f0a08391 | ||
|
|
2d683e7e46 | ||
|
|
247f6353b0 | ||
|
|
08e28fd0a5 | ||
|
|
cab591eb48 | ||
|
|
33e5acf0ea | ||
|
|
15554ef767 | ||
|
|
a9cb03c26f | ||
|
|
fb6110d0eb | ||
|
|
c3ace1faff | ||
|
|
c5286c3a6e | ||
|
|
1316eb2769 | ||
|
|
27909f1824 | ||
|
|
78c852514f | ||
|
|
985a681d72 | ||
|
|
3c79840a29 | ||
|
|
95ee614d9f | ||
|
|
591ce5274f | ||
|
|
3f18951368 | ||
|
|
be077f1a67 | ||
|
|
1a4b53917d | ||
|
|
726e13edcd | ||
|
|
4ebd990e30 | ||
|
|
4567550978 | ||
|
|
f489bd7d36 | ||
|
|
e93e42f952 | ||
|
|
250c9c4bce | ||
|
|
aa46a62457 | ||
|
|
f66c92aefb | ||
|
|
a696611615 | ||
|
|
5db53b5a9e | ||
|
|
e9e732756e | ||
|
|
ca96b85352 | ||
|
|
67f5098c95 | ||
|
|
f37652942d | ||
|
|
f67d182a92 | ||
|
|
b325cc75c0 | ||
|
|
6cc76903b0 | ||
|
|
b4bc0b006a | ||
|
|
b0ce899a71 | ||
|
|
bd162a9e08 | ||
|
|
8b7d69c0ff | ||
|
|
518f8b402e | ||
|
|
397457cacb | ||
|
|
eee0db133c | ||
|
|
dec05198c7 | ||
|
|
911c3c17c5 | ||
|
|
9d852eee9c | ||
|
|
17e1f1e4c7 | ||
|
|
c0ac29fdd8 | ||
|
|
9b3f93688e | ||
|
|
f4f7d9addc | ||
|
|
4e35f9f290 | ||
|
|
820b6ea3aa | ||
|
|
647f900b85 | ||
|
|
6c0f97ffe9 | ||
|
|
bc95b905af | ||
|
|
330ac54c6f | ||
|
|
54249d03dd | ||
|
|
d543393e3f | ||
|
|
ee010de36c | ||
|
|
3c7aaafa63 | ||
|
|
b62cec0cdc | ||
|
|
a474660498 | ||
|
|
6e2a5ff081 | ||
|
|
8dd63c722f | ||
|
|
e98dff325d | ||
|
|
8d93508062 | ||
|
|
fd0411b508 | ||
|
|
8564ed255a | ||
|
|
9d78700ab7 | ||
|
|
c72fedbb30 | ||
|
|
e637c61cc9 | ||
|
|
dae19ca067 | ||
|
|
9d6fc68642 | ||
|
|
2cd8b96d38 | ||
|
|
541337259d | ||
|
|
93defcd3a7 | ||
|
|
69edbbdf42 | ||
|
|
dcea005d8d | ||
|
|
cfd59d31a1 | ||
|
|
f42ffa61d1 | ||
|
|
0a2254702f | ||
|
|
d02327af2d | ||
|
|
c10ca2a77e | ||
|
|
2f8c01418d | ||
|
|
3a0c84a8f5 | ||
|
|
597b443796 | ||
|
|
0ea925289b | ||
|
|
f3b116e47c | ||
|
|
4c3ec7d067 | ||
|
|
941d8afb9a | ||
|
|
8e41662088 | ||
|
|
71bb2d889b | ||
|
|
c712473c7a | ||
|
|
121515cbc7 | ||
|
|
8b52dc8acb | ||
|
|
db5644e058 | ||
|
|
684cc666d3 | ||
|
|
dfff608ad1 | ||
|
|
f1c5ae73c2 | ||
|
|
d85ec7bcfb | ||
|
|
2855b1bb0b | ||
|
|
2a616e2518 | ||
|
|
d5d131185e | ||
|
|
0f2c0e794c | ||
|
|
d4e697c0de | ||
|
|
720dde1c06 | ||
|
|
1e017475ff | ||
|
|
95e51d1c27 | ||
|
|
d653a54fc3 | ||
|
|
c2ccccd093 | ||
|
|
837fa03f64 | ||
|
|
f027b4795f | ||
|
|
6ede54e3e0 | ||
|
|
922d259193 | ||
|
|
56075b5766 | ||
|
|
b0f02c6a36 | ||
|
|
de1326e118 | ||
|
|
53d1d1fded | ||
|
|
e17f427a5d | ||
|
|
df5af7d60c | ||
|
|
702cb0d140 | ||
|
|
9e415bff54 | ||
|
|
a696a0e0b2 | ||
|
|
52026701e4 | ||
|
|
7471488e33 | ||
|
|
0ef8a0b2d4 | ||
|
|
76a72a5ead | ||
|
|
3b0cba493b | ||
|
|
bc9f97f852 | ||
|
|
295eb2bd60 | ||
|
|
dfcf9de78e | ||
|
|
fc6d63603a | ||
|
|
9156ad915a | ||
|
|
a138a922ef | ||
|
|
b51ff6583d | ||
|
|
7207ad6da7 | ||
|
|
e3fafbc6ff | ||
|
|
be5eb26385 | ||
|
|
e9b529a644 | ||
|
|
250a1bcc81 | ||
|
|
96edb065d7 | ||
|
|
7e501a460a | ||
|
|
dd05ba239e | ||
|
|
b373fc465b | ||
|
|
f92f7ca803 | ||
|
|
51062a2499 | ||
|
|
48a3967b82 | ||
|
|
18ef16f6ee | ||
|
|
fda3ada055 | ||
|
|
db5500f130 | ||
|
|
42bbd0fe01 | ||
|
|
b257752812 | ||
|
|
54b7c8d697 | ||
|
|
bcafd0790f | ||
|
|
e07da9cad9 | ||
|
|
d3b5befe3e | ||
|
|
4481d94fa2 | ||
|
|
982a6dd5f2 | ||
|
|
898fc8c13c | ||
|
|
9ec2762bff | ||
|
|
6d9059ee86 | ||
|
|
ef6ab620fb | ||
|
|
ecf4cb15be | ||
|
|
c9d36b5bad | ||
|
|
9e932799ee | ||
|
|
6215b65ec9 | ||
|
|
9adf64c374 | ||
|
|
d32f9533e3 | ||
|
|
2a0a8585df | ||
|
|
87a2b219a5 | ||
|
|
c5e13ccc84 | ||
|
|
40c9a18be4 | ||
|
|
8a4aa7908f | ||
|
|
82879e39e7 | ||
|
|
60534bcfe3 | ||
|
|
41ddd7c1a6 | ||
|
|
5601a556c9 | ||
|
|
764e0f3f25 | ||
|
|
3619807cb9 | ||
|
|
d65f220530 | ||
|
|
903e758528 | ||
|
|
9b85f00564 | ||
|
|
cb191d60f3 | ||
|
|
ac70fcc5f9 | ||
|
|
68d24919b1 | ||
|
|
baa411d9c7 | ||
|
|
7e199f6815 | ||
|
|
9ca0649368 | ||
|
|
987b6b2f82 | ||
|
|
bf3a4dcc7a | ||
|
|
e4cfd85dc7 | ||
|
|
974b6a9ea2 | ||
|
|
71345465cf | ||
|
|
89c02c9a96 | ||
|
|
73382db4a2 | ||
|
|
9bafd037c3 | ||
|
|
57e3a369ae | ||
|
|
f0550d75b8 | ||
|
|
b694c837b4 | ||
|
|
a083b06de0 | ||
|
|
17e9599976 | ||
|
|
487b96d7d3 | ||
|
|
83b5319433 | ||
|
|
db305db392 | ||
|
|
dfd5f9dd94 | ||
|
|
8748d8b4e5 | ||
|
|
f97e9f9333 | ||
|
|
3c58c6bf5f | ||
|
|
a4899658df | ||
|
|
1053740723 | ||
|
|
b0adfb97ad | ||
|
|
d0d2314e9a | ||
|
|
22aafce24e | ||
|
|
c02dfafa2a | ||
|
|
f8b22dc1ed | ||
|
|
4a9b543a3a | ||
|
|
1332a73189 | ||
|
|
165a6695b9 | ||
|
|
b816745830 | ||
|
|
0b198b4031 | ||
|
|
ffdab44710 | ||
|
|
3e423a14f4 | ||
|
|
f56c65df79 | ||
|
|
c8eb0914b3 | ||
|
|
05851fb728 | ||
|
|
5c12810d23 | ||
|
|
e11392d0af | ||
|
|
e3f2da13f5 | ||
|
|
a94eeb023d | ||
|
|
82162b167b | ||
|
|
7100d251ec | ||
|
|
c4f98a10dc | ||
|
|
9585c92629 | ||
|
|
2594427e75 | ||
|
|
49178b50b0 | ||
|
|
353a7c9c2b | ||
|
|
a9614c15aa | ||
|
|
690e2f6e85 | ||
|
|
16b9122f9e | ||
|
|
4cfe16a708 | ||
|
|
3c4aaf5bba | ||
|
|
bc2fe382a9 | ||
|
|
a0fe1080f3 | ||
|
|
81eb07ee98 | ||
|
|
f7de2bb6d9 | ||
|
|
d430f835a4 | ||
|
|
c0f0ce7df2 | ||
|
|
d934fb41ab | ||
|
|
d0739ce0f1 | ||
|
|
545b5db143 | ||
|
|
649a5cb86a | ||
|
|
88c1595deb | ||
|
|
a8498bb7f6 | ||
|
|
d5fa496a20 | ||
|
|
9f86e8355d | ||
|
|
8f4784f86d | ||
|
|
696a489f43 | ||
|
|
67d436cfc5 | ||
|
|
a966de9f50 | ||
|
|
9b7e8250bc | ||
|
|
7723c0149d | ||
|
|
38c363162b | ||
|
|
4a46c46253 | ||
|
|
9d1349b5aa | ||
|
|
2c857a6eb1 | ||
|
|
ad64ced2df | ||
|
|
81fb54cff5 | ||
|
|
45bbc93bee | ||
|
|
f9542f9214 | ||
|
|
60052b8ab7 | ||
|
|
7b9a6b4e9c | ||
|
|
09ca2a6740 | ||
|
|
20cd27e68d | ||
|
|
129d061434 | ||
|
|
19f59b207f | ||
|
|
528028d41b | ||
|
|
d77fa67729 | ||
|
|
f33418ec35 | ||
|
|
dd379c41c9 | ||
|
|
609a25119d | ||
|
|
9258acf765 | ||
|
|
d46d90395c | ||
|
|
b4e97ead04 | ||
|
|
ea5574bfa5 | ||
|
|
5b6e8de9a1 | ||
|
|
9980ec62fd | ||
|
|
8abb9b4358 | ||
|
|
40c1c0ec02 | ||
|
|
f2bc95ffcc | ||
|
|
4b1fbb1243 | ||
|
|
e7c2a8b880 | ||
|
|
dbbe9e8b0e | ||
|
|
b26a12074f | ||
|
|
79f99bd4f1 | ||
|
|
38cfe3a23b | ||
|
|
5e27701228 | ||
|
|
0a9fe9bb89 | ||
|
|
d0cd206f51 | ||
|
|
9d5f11aed5 | ||
|
|
eba5e422bf | ||
|
|
0e6f200efe | ||
|
|
e890a603c2 | ||
|
|
97951e7e8e | ||
|
|
3d98b64a42 | ||
|
|
d3dfd54f54 | ||
|
|
2e15732f64 | ||
|
|
45f5c5368a | ||
|
|
012e3b5f3c | ||
|
|
35521a5214 | ||
|
|
ad1e8d03e4 | ||
|
|
1eef1e083d | ||
|
|
bb5215c306 | ||
|
|
2c47b4e585 | ||
|
|
4371eb7c39 | ||
|
|
e16a3d46b3 | ||
|
|
1a3996e73b | ||
|
|
c3006c69a6 | ||
|
|
523bcbb6e3 | ||
|
|
127ad155ba | ||
|
|
14d287afe3 | ||
|
|
6231977e70 | ||
|
|
758b3e804e | ||
|
|
67a6d8194d | ||
|
|
31b5a3df77 | ||
|
|
785cf9c73a | ||
|
|
d9aa319e2c | ||
|
|
141e7233ff | ||
|
|
9ff25e7c3a | ||
|
|
c9fe54ed29 | ||
|
|
bbaef6cf8e | ||
|
|
7df1432744 | ||
|
|
6442d85c9f | ||
|
|
f4ad2ea6c8 | ||
|
|
f7fdf3f987 | ||
|
|
b8833b74eb | ||
|
|
bc29d1e86c | ||
|
|
5ea9b8211e | ||
|
|
25f1881f85 | ||
|
|
d819d8e8a1 | ||
|
|
db022a0688 | ||
|
|
c88426e0b2 | ||
|
|
4818345ce2 | ||
|
|
70ee681757 | ||
|
|
9e03d5f665 | ||
|
|
1561f2631a | ||
|
|
c4ae5496e2 | ||
|
|
53692ffc7c | ||
|
|
3b0194d886 | ||
|
|
e75a1ff4fe | ||
|
|
042c8061f6 | ||
|
|
865570fa94 | ||
|
|
5e7f44b5ca | ||
|
|
821161363e | ||
|
|
e542da8c74 | ||
|
|
31fb795fd8 | ||
|
|
5cf7f99517 | ||
|
|
3f64463580 | ||
|
|
1a9f16a25a | ||
|
|
1084d87399 | ||
|
|
3147a6989c | ||
|
|
63502d5f30 | ||
|
|
06733812e6 | ||
|
|
f9f1c19b84 | ||
|
|
01b3a428d9 | ||
|
|
fd2f14ae2e | ||
|
|
bf6c9121f4 | ||
|
|
a67d1231de | ||
|
|
b69f5f4b5f | ||
|
|
8605b23135 | ||
|
|
7d823dd61d | ||
|
|
8152c3cebf | ||
|
|
7229b8a0a0 | ||
|
|
44a5630634 | ||
|
|
19a65050e8 | ||
|
|
04948a21f0 | ||
|
|
f7f4430be9 | ||
|
|
e77f3ed44a | ||
|
|
180ba935b1 | ||
|
|
0e106e263b | ||
|
|
7327917a81 | ||
|
|
c08dcd9fb5 | ||
|
|
ee6785f6ab | ||
|
|
537ba26645 | ||
|
|
e604ee2440 | ||
|
|
6534a2d62e | ||
|
|
b5c3bedd14 | ||
|
|
10bbaa9217 | ||
|
|
455e16a88f | ||
|
|
5d27a19b38 | ||
|
|
7cab722df3 | ||
|
|
b99c988970 | ||
|
|
f53c070ca1 | ||
|
|
84752e0fef | ||
|
|
0d4e0f1dab | ||
|
|
cea0cff2a7 | ||
|
|
a60f1e13db | ||
|
|
abe2e0b73b | ||
|
|
f9bb33c49b | ||
|
|
7a30b18c20 | ||
|
|
3fb9a195f8 | ||
|
|
c386a0c439 | ||
|
|
216c5d8b8e | ||
|
|
b57a39d8e8 | ||
|
|
8ba828885b | ||
|
|
d2c38963c9 | ||
|
|
59d00c1765 | ||
|
|
8217ad8fa5 | ||
|
|
4b2f134f32 | ||
|
|
6526caae30 | ||
|
|
4f26bc20a0 | ||
|
|
8b9b0b4fcb | ||
|
|
d2d5016780 | ||
|
|
a09dbbfd27 | ||
|
|
82cfb62804 | ||
|
|
ebf445e3e1 | ||
|
|
2123af39a7 | ||
|
|
917e661e88 | ||
|
|
bf905b1562 | ||
|
|
f6fcac5856 | ||
|
|
d4f11542f9 | ||
|
|
f906c0ecab | ||
|
|
99f8adeb7d | ||
|
|
0965a78d46 | ||
|
|
fe38b53ff7 | ||
|
|
1065c0d440 | ||
|
|
fd84a9a5fc | ||
|
|
b62c422959 | ||
|
|
f2305429c4 | ||
|
|
292e6b0339 | ||
|
|
7b54873f08 | ||
|
|
281b3ed45b | ||
|
|
8b2d659000 | ||
|
|
4c31740f25 | ||
|
|
848ae5fc9d | ||
|
|
b5da4e00ca | ||
|
|
4c3688de12 | ||
|
|
805660d4d1 | ||
|
|
1821deaa73 | ||
|
|
c9fd9a2a46 | ||
|
|
31bd0a6d68 | ||
|
|
c3162bbb1f | ||
|
|
02451e8ec8 | ||
|
|
bd21332c25 | ||
|
|
71f1609467 | ||
|
|
5331bc630a | ||
|
|
ea05c0327b | ||
|
|
d207f86daa | ||
|
|
9c26d54eda | ||
|
|
b72c4db334 | ||
|
|
5830958478 | ||
|
|
c78481b5b1 | ||
|
|
0af9e0f42e | ||
|
|
48b0b25974 | ||
|
|
6bd303d2e8 | ||
|
|
dbddcc1c10 | ||
|
|
bb4a2b9b97 | ||
|
|
5c3b5031d8 | ||
|
|
b460443e77 | ||
|
|
69bfb4180c | ||
|
|
2d32823db8 | ||
|
|
38262f11b2 | ||
|
|
e32beea00a | ||
|
|
bfcbaacec7 | ||
|
|
be8631e90c | ||
|
|
4198881815 | ||
|
|
b91696bc89 | ||
|
|
f7d070b38c | ||
|
|
95cc29fd9c | ||
|
|
9ae76a90fc | ||
|
|
24b8617c54 | ||
|
|
892a168ee7 | ||
|
|
51336412c5 | ||
|
|
f38913b75c | ||
|
|
ae35474130 | ||
|
|
bb5cc236a7 | ||
|
|
6a6c80d9a4 | ||
|
|
ae8f7964fa | ||
|
|
3630b17cde | ||
|
|
95f97d33af | ||
|
|
1243de8010 | ||
|
|
965b179d56 | ||
|
|
5d0fff2276 | ||
|
|
d7b6b7c0ae | ||
|
|
de0497a2dc | ||
|
|
1653c16c65 | ||
|
|
88783b3f5f | ||
|
|
d701bc7347 | ||
|
|
82e9ca93b6 | ||
|
|
5f99e7280a | ||
|
|
f56bf3d41b | ||
|
|
1ac0494f96 | ||
|
|
08720d8eeb | ||
|
|
c44057f47d | ||
|
|
abd54b3939 | ||
|
|
1a9fc593d1 | ||
|
|
b1bf86724d | ||
|
|
bc4c0cc505 | ||
|
|
69a5ffd8b2 | ||
|
|
e22f2f2e90 | ||
|
|
b02972b7ea | ||
|
|
d3cabc1c05 | ||
|
|
634cf79916 | ||
|
|
7e660091e0 | ||
|
|
296ced8e9b | ||
|
|
1226caca03 | ||
|
|
f1c02442bc | ||
|
|
52f8f513da | ||
|
|
c22b364ebe | ||
|
|
0792838b16 | ||
|
|
f16f6ea4d7 | ||
|
|
2e9cc3b581 | ||
|
|
71543dc615 | ||
|
|
f4f327e7d4 | ||
|
|
4c6777862e | ||
|
|
7acee37607 | ||
|
|
1bb9fd04d8 | ||
|
|
022806bea2 | ||
|
|
55bbfe59e6 | ||
|
|
52d7d21019 | ||
|
|
6974de02ac | ||
|
|
d46bd8d10c | ||
|
|
9129ff6e75 | ||
|
|
ed102015ad | ||
|
|
26e2375bfb | ||
|
|
bb1de75128 | ||
|
|
254f238f82 | ||
|
|
85210e2935 | ||
|
|
cdb461ab68 | ||
|
|
0569ed0f86 | ||
|
|
64d1e8677e | ||
|
|
790f6440f3 | ||
|
|
9e46bd8e2c | ||
|
|
02590579df | ||
|
|
fccae7734d | ||
|
|
f97cd74bd3 | ||
|
|
9b5cd427e6 | ||
|
|
0b85b09f62 | ||
|
|
6887291151 | ||
|
|
26b72e3792 | ||
|
|
2d042f2c44 | ||
|
|
52c9e644b8 | ||
|
|
058a0f82d3 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
github: lwouis
|
||||
patreon: alt_tab_macos
|
||||
custom: ["paypal.com/donate/?hosted_button_id=ACYAC3UYCM7CN", "donate.stripe.com/4gw9D7bA3g3O0ikaEF"]
|
||||
@@ -13,7 +13,7 @@ A clear and concise description of what the bug is. What happened, and what you
|
||||
|
||||
**Screenshots / video**
|
||||
|
||||
Add screenshots to help explain your problem. You can also use Loom to easily record your screen to show the bug in action. That is *really* helpful!
|
||||
Add screenshots to help explain your problem. You can also record your screen to show the bug in action. That is *really* helpful!
|
||||
|
||||
**Steps to reproduce the bug**
|
||||
|
||||
@@ -21,3 +21,9 @@ Add screenshots to help explain your problem. You can also use Loom to easily re
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Your environment**
|
||||
|
||||
* AltTab version:
|
||||
* macOS version:
|
||||
* Other relevant info: e.g. using external keyboard, multiple monitors, etc
|
||||
@@ -0,0 +1,67 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPCENTER_SECRET: ${{ secrets.APPCENTER_SECRET }}
|
||||
APPCENTER_TOKEN: ${{ secrets.APPCENTER_TOKEN }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_P12_CERTIFICATE: ${{ secrets.APPLE_P12_CERTIFICATE }}
|
||||
FEEDBACK_TOKEN: ${{ secrets.FEEDBACK_TOKEN }}
|
||||
NETLIFY_WEBHOOK: ${{ secrets.NETLIFY_WEBHOOK }}
|
||||
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
|
||||
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
GITHUB_EVENT_AFTER: ${{ github.event.after }}
|
||||
BUILD_DIR: ${{ github.workspace }}
|
||||
XCODE_BUILD_PATH: DerivedData/Build/Products/Release
|
||||
VERSION_FILE: VERSION.txt
|
||||
APP_NAME: AltTab
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-15 # all options: https://github.com/actions/runner-images/tree/main
|
||||
environment: production
|
||||
steps:
|
||||
# - env:
|
||||
# GITHUB_CONTEXT: ${{ toJSON(github) }}
|
||||
# run: echo "$GITHUB_CONTEXT"
|
||||
- id: pre-checkout
|
||||
env:
|
||||
COMMITS: ${{ toJson(github.event.commits) }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
COUNT=$(( $(echo "$COMMITS" | jq length) + 1 ))
|
||||
else
|
||||
COUNT=1
|
||||
fi
|
||||
echo "fetch-depth=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: ${{ steps.pre-checkout.outputs.fetch-depth }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
- run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
|
||||
- run: scripts/print_env.sh
|
||||
- run: npm ci
|
||||
- run: npx commitlint --from "$GITHUB_EVENT_BEFORE" --to "$GITHUB_EVENT_AFTER" --verbose
|
||||
- run: scripts/ensure_generated_files_are_up_to_date.sh
|
||||
- run: scripts/determine_next_version.sh
|
||||
- run: scripts/replace_environment_variables_in_app.sh
|
||||
- run: scripts/codesign/setup_ci_master.sh
|
||||
- run: scripts/run_tests.sh
|
||||
- run: scripts/build_app.sh
|
||||
- run: scripts/package_and_notarize_release.sh
|
||||
- run: scripts/upload_symbols_to_appcenter.sh
|
||||
- run: scripts/update_appcast.sh
|
||||
- run: npx semantic-release
|
||||
- id: set_tag_name
|
||||
run: echo "tag_name=v"$(cat $VERSION_FILE)"" >> "$GITHUB_OUTPUT"
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
|
||||
files: ${{ env.XCODE_BUILD_PATH }}/*.zip
|
||||
- run: scripts/update_website.sh
|
||||
@@ -1,12 +1,15 @@
|
||||
/.idea/
|
||||
xcuserdata/
|
||||
/.vscode/
|
||||
/buildServer.json
|
||||
WorkspaceSettings.xcsettings
|
||||
IDEWorkspaceChecks.plist
|
||||
/node_modules/
|
||||
/DerivedData/
|
||||
/build/
|
||||
.DS_Store
|
||||
docs/_site/
|
||||
/docs/_site/
|
||||
/docs/vendor/
|
||||
codesign.conf
|
||||
codesign.crt
|
||||
codesign.key
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
retain_objc_accessible: true
|
||||
schemes:
|
||||
- Release
|
||||
targets:
|
||||
- alt-tab-macos
|
||||
workspace: alt-tab-macos.xcworkspace
|
||||
report_exclude:
|
||||
- src/api-wrappers/PrivateApis.swift
|
||||
@@ -0,0 +1,10 @@
|
||||
# rules: https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md
|
||||
|
||||
--swiftversion 5.8
|
||||
--maxwidth 110
|
||||
--nowrapoperators .,==,!=,<,>,>=,<=
|
||||
--wrapternary before-operators
|
||||
--disable andOperator
|
||||
--disable wrapConditionalBodies
|
||||
--disable wrapLoopBodies
|
||||
--disable wrapMultilineStatementBraces
|
||||
@@ -0,0 +1,11 @@
|
||||
# **/*.swift: Matches all .swift files in the current directory and all its subdirectories.
|
||||
# src/**/*.swift: Matches all .swift files in the src directory and all its subdirectories.
|
||||
# !node_modules/**: Excludes all files in the node_modules directory and all its subdirectories.
|
||||
|
||||
Pods/**
|
||||
Generated/**
|
||||
|
||||
**/PrivateApis.swift
|
||||
|
||||
# We will open a new branch to deal with code format issues at the next release.
|
||||
**/*
|
||||
@@ -1,33 +0,0 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
os: osx
|
||||
osx_image: xcode12.3
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
addons:
|
||||
ssh_known_hosts: github.com
|
||||
homebrew:
|
||||
packages:
|
||||
- vitorgalvao/tiny-scripts/cask-repair
|
||||
update: true
|
||||
env:
|
||||
global:
|
||||
- IS_RELEASE=$(if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then echo true; fi)
|
||||
- XCODE_BUILD_PATH="DerivedData/Build/Products/Release"
|
||||
- VERSION_FILE="VERSION.txt"
|
||||
- APP_NAME="$(awk -F ' = ' '/PRODUCT_NAME/ { print $2; }' < config/base.xcconfig)"
|
||||
install: true # disable default install
|
||||
script: scripts/travis.sh
|
||||
deploy:
|
||||
provider: releases
|
||||
token:
|
||||
secure: wcAMT2rbXhhdk6lsc6EG9VZZkSW0BjOwjYSyXzHjKrgiXsQcZAzAsOkuisj8EfeYN9/uhpa8XX7f5KsdkE5reMqVGGWqdmJU4NnmK5Aew0LmBbzFHfffliQecVCzMmGYPmhPcDxAjidYxgCMrMbvQFaElc0WXdIsQNcaNieSkEgZw6TwGsTxgHXABD2e575K3SN/PPcq7Nx5mRZkURI7YLmo2KfsD2OPR4IcEXFpJRFbPcAZIpKaHUIC2VzpayJA93pBe+siwU150p04PzfSs/nTH/nUO9m24SUi6kAIzVN77cExvX+JDnQoVLz0V45L0iJjkKDTVb07rVOsfm6Pcuaaj2ZVXlpujSb7B5LDUIspUn/NnqMLOtzbSTuaGcx2u+zCcC0b17KooznilQ/sFq3YpeNdmjL4VL5fsw1hdpjcP6S9DqZ90TJZqy6xBQP5NubuJukH//BRilhWGfnq5rOFUlqrN8aApJ7gYry0K2rB8rVWWvA9cU+zX351iy0EERICEU6q3rQwq8gFUbdNATPJJgNg7n0PaNXSQTTF9VluZTRVUtMrpA0hWe7dn+Tvrvhrnhmqvccw3Qfw/i3SECIDHHtsjwLqHCx5gkz/xUdf7+t3k6x+r6Veua0Z3jvyVhdHprU47VTSUhM/n87EiKyNRuKa9IpA18eSlZ59exU=
|
||||
file_glob: true
|
||||
file: "$XCODE_BUILD_PATH/*.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: lwouis/alt-tab-macos
|
||||
after_deploy: scripts/update_homebrew_cask.sh
|
||||
@@ -6,6 +6,12 @@ verify_ssl = true
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
fonttools = "*"
|
||||
fonttools = "==4.8.1"
|
||||
googletrans = "==3.0.0"
|
||||
|
||||
[scripts]
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
[scripts]
|
||||
font2png = "scripts/convert_font_to_png.sh"
|
||||
translate = "python scripts/l10n/google_translate_all_terms_locally.py resources/l10n --retry_translate"
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "8f11c81dff5e32871b7cb2314acb76fd1fb4a06fdb403a65bc6749fe3770eda6"
|
||||
"sha256": "3646221352b85667ac21f1b528dc8b29f56ff1e24cb97e7eaba79b5040c78e21"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
@@ -14,13 +16,111 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407",
|
||||
"sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2025.8.3"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"fonttools": {
|
||||
"hashes": [
|
||||
"sha256:596c0e8399c650dbf1048b7ca6fc7b12bd79a7db0981f3b87743de8c2b7512de",
|
||||
"sha256:6d241d319ea9eae4c4ee27cbd1db8656dbd496dd9aa884401debfab3a4c7d82b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.8.1"
|
||||
},
|
||||
"googletrans": {
|
||||
"hashes": [
|
||||
"sha256:44caeea42d91ff6ead5c2d49db2b88de66c45c2fe874c8ec03eb9b4ceb3a533d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1",
|
||||
"sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"h2": {
|
||||
"hashes": [
|
||||
"sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5",
|
||||
"sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
},
|
||||
"hpack": {
|
||||
"hashes": [
|
||||
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
|
||||
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"hstspreload": {
|
||||
"hashes": [
|
||||
"sha256:02bd5b0885b62f0d5239ca6c6876ffcad8d9c08dc97cee54292598141a16e9dc",
|
||||
"sha256:346552a807b3a1762376de8ecce097544e7fcd64fb64231b4652da52f86fa6f1"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2025.1.1"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0",
|
||||
"sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"httpx": {
|
||||
"hashes": [
|
||||
"sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335",
|
||||
"sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.13.3"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
|
||||
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
|
||||
],
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
|
||||
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
||||
@@ -1,10 +1,42 @@
|
||||
platform :osx, '10.12'
|
||||
def deployment_target_from_xcconfig()
|
||||
xcconfig_path = 'config/base.xcconfig'
|
||||
File.foreach(xcconfig_path) do |line|
|
||||
if line.start_with?('MACOSX_DEPLOYMENT_TARGET')
|
||||
target = line.split("=").last.strip
|
||||
puts "MACOSX_DEPLOYMENT_TARGET: #{target}"
|
||||
return target
|
||||
end
|
||||
end
|
||||
puts "\e[31mCouldn't read MACOSX_DEPLOYMENT_TARGET from #{xcconfig_path}\e[0m"
|
||||
exit 1
|
||||
end
|
||||
|
||||
deployment_target = deployment_target_from_xcconfig()
|
||||
|
||||
platform :osx, deployment_target
|
||||
|
||||
target 'alt-tab-macos' do
|
||||
use_frameworks!
|
||||
pod 'LetsMove', :git => 'https://github.com/lwouis/LetsMove.git', :commit => '7abf4daed1a25218f2b52f2dfd190aee5a50071c'
|
||||
pod 'Sparkle', :podspec => 'https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec'
|
||||
pod 'ShortcutRecorder', :git => 'https://github.com/Kentzo/ShortcutRecorder.git', :commit => 'f718220854575bad86fa89dd2bb3decd0b14b2b0'
|
||||
pod 'SwiftyMarkdown', '1.1.0'
|
||||
pod 'ShortcutRecorder', :git => 'https://github.com/lwouis/ShortcutRecorder.git', :branch => 'alt-tab-current'
|
||||
pod 'AppCenter/Crashes', '4.3.0'
|
||||
pod 'SwiftyBeaver', '1.9.0'
|
||||
end
|
||||
|
||||
target 'unit-tests' do
|
||||
use_frameworks!
|
||||
pod 'ShortcutRecorder', :git => 'https://github.com/lwouis/ShortcutRecorder.git', :branch => 'alt-tab-current'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# disable code signing which is unnecessary for pods
|
||||
config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
||||
# set deployment_target for all pods, to avoid libarclite compiler error
|
||||
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = deployment_target
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,27 +5,27 @@ PODS:
|
||||
- LetsMove (1.24)
|
||||
- ShortcutRecorder (3.1)
|
||||
- Sparkle (1.24.0)
|
||||
- SwiftyMarkdown (1.1.0)
|
||||
- SwiftyBeaver (1.9.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AppCenter/Crashes (= 4.3.0)
|
||||
- LetsMove (from `https://github.com/lwouis/LetsMove.git`, commit `7abf4daed1a25218f2b52f2dfd190aee5a50071c`)
|
||||
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, commit `f718220854575bad86fa89dd2bb3decd0b14b2b0`)
|
||||
- ShortcutRecorder (from `https://github.com/lwouis/ShortcutRecorder.git`, branch `alt-tab-current`)
|
||||
- Sparkle (from `https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec`)
|
||||
- SwiftyMarkdown (= 1.1.0)
|
||||
- SwiftyBeaver (= 1.9.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppCenter
|
||||
- SwiftyMarkdown
|
||||
- SwiftyBeaver
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
LetsMove:
|
||||
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
|
||||
:git: https://github.com/lwouis/LetsMove.git
|
||||
ShortcutRecorder:
|
||||
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
|
||||
:git: https://github.com/Kentzo/ShortcutRecorder.git
|
||||
:branch: alt-tab-current
|
||||
:git: https://github.com/lwouis/ShortcutRecorder.git
|
||||
Sparkle:
|
||||
:podspec: https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec
|
||||
|
||||
@@ -34,16 +34,16 @@ CHECKOUT OPTIONS:
|
||||
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
|
||||
:git: https://github.com/lwouis/LetsMove.git
|
||||
ShortcutRecorder:
|
||||
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
|
||||
:git: https://github.com/Kentzo/ShortcutRecorder.git
|
||||
:commit: 594b360e07a8a368ffec2567f77e465477b9994f
|
||||
:git: https://github.com/lwouis/ShortcutRecorder.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppCenter: 883369ab78427b0561c688158d689dfe1f993ea9
|
||||
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
|
||||
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
|
||||
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
|
||||
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
|
||||
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
|
||||
|
||||
PODFILE CHECKSUM: 221e0c8a87ed30b40d9d155eb61d139c2205b497
|
||||
PODFILE CHECKSUM: c3fc354fa5ccad08f1b9f7302205cc969b4b0ef0
|
||||
|
||||
COCOAPODS: 1.9.1
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -5,27 +5,27 @@ PODS:
|
||||
- LetsMove (1.24)
|
||||
- ShortcutRecorder (3.1)
|
||||
- Sparkle (1.24.0)
|
||||
- SwiftyMarkdown (1.1.0)
|
||||
- SwiftyBeaver (1.9.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AppCenter/Crashes (= 4.3.0)
|
||||
- LetsMove (from `https://github.com/lwouis/LetsMove.git`, commit `7abf4daed1a25218f2b52f2dfd190aee5a50071c`)
|
||||
- ShortcutRecorder (from `https://github.com/Kentzo/ShortcutRecorder.git`, commit `f718220854575bad86fa89dd2bb3decd0b14b2b0`)
|
||||
- ShortcutRecorder (from `https://github.com/lwouis/ShortcutRecorder.git`, branch `alt-tab-current`)
|
||||
- Sparkle (from `https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec`)
|
||||
- SwiftyMarkdown (= 1.1.0)
|
||||
- SwiftyBeaver (= 1.9.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppCenter
|
||||
- SwiftyMarkdown
|
||||
- SwiftyBeaver
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
LetsMove:
|
||||
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
|
||||
:git: https://github.com/lwouis/LetsMove.git
|
||||
ShortcutRecorder:
|
||||
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
|
||||
:git: https://github.com/Kentzo/ShortcutRecorder.git
|
||||
:branch: alt-tab-current
|
||||
:git: https://github.com/lwouis/ShortcutRecorder.git
|
||||
Sparkle:
|
||||
:podspec: https://raw.githubusercontent.com/lwouis/Sparkle/fix-iframe-popup-1.24/Sparkle.podspec
|
||||
|
||||
@@ -34,16 +34,16 @@ CHECKOUT OPTIONS:
|
||||
:commit: 7abf4daed1a25218f2b52f2dfd190aee5a50071c
|
||||
:git: https://github.com/lwouis/LetsMove.git
|
||||
ShortcutRecorder:
|
||||
:commit: f718220854575bad86fa89dd2bb3decd0b14b2b0
|
||||
:git: https://github.com/Kentzo/ShortcutRecorder.git
|
||||
:commit: 594b360e07a8a368ffec2567f77e465477b9994f
|
||||
:git: https://github.com/lwouis/ShortcutRecorder.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppCenter: 883369ab78427b0561c688158d689dfe1f993ea9
|
||||
LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5
|
||||
ShortcutRecorder: fdf620aca34101b0cba3b10fca815e0459254189
|
||||
Sparkle: 1fa490bb4ab2a9b37cfb9ea4feec19dd33b81cd2
|
||||
SwiftyMarkdown: 033abc9dad9a37903f81aec5188cade436f40520
|
||||
SwiftyBeaver: d39e7c376bf15e9fffca8263b8fd1f42ea56aa2d
|
||||
|
||||
PODFILE CHECKSUM: 221e0c8a87ed30b40d9d155eb61d139c2205b497
|
||||
PODFILE CHECKSUM: c3fc354fa5ccad08f1b9f7302205cc969b4b0ef0
|
||||
|
||||
COCOAPODS: 1.9.1
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -318,6 +318,10 @@ typedef TISInputSourceRef (*_SRKeyCodeTransformerCacheInputSourceCreate)(void);
|
||||
explicitModifierFlags:0
|
||||
usingCache:YES];
|
||||
|
||||
// fix: https://github.com/lwouis/alt-tab-macos/issues/1190
|
||||
if (newTranslationToKeyCode[translation])
|
||||
continue;
|
||||
|
||||
if (translation.length)
|
||||
newTranslationToKeyCode[translation] = keyCode;
|
||||
}
|
||||
|
||||
@@ -442,8 +442,8 @@ NSString *const SRShortcutCharactersIgnoringModifiers = SRShortcutKeyCharactersI
|
||||
|
||||
- (UInt32)carbonKeyCode
|
||||
{
|
||||
if (self.keyCode == SRKeyCodeNone)
|
||||
os_trace_error("#Critical SRKeyCodeNone has no representation in Carbon");
|
||||
// if (self.keyCode == SRKeyCodeNone)
|
||||
// os_trace_error("#Critical SRKeyCodeNone has no representation in Carbon");
|
||||
|
||||
return self.keyCode;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
Copyright (c) 2015 Sebastian Kreutzberger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -0,0 +1,270 @@
|
||||
<p align="center"><a href="https://swiftybeaver.com"><img src="https://cloud.githubusercontent.com/assets/564725/19889302/73b1ee84-a034-11e6-8753-2d060502397c.jpg" style="width: 888px;" alt="SwiftyBeaver"></a><br/><b>Colorful</b>, flexible, <b>lightweight</b> logging for Swift 3, Swift 4 & <b>Swift 5</b>.<br/>Great for <b>development & release</b> with support for Console, File & cloud platforms.<br/>Log <b>during release</b> to the conveniently built-in SwiftyBeaver Platform, the <b>dedicated Mac App</b> & <b>Elasticsearch</b>!<br/><br/><a href="http://docs.swiftybeaver.com">Docs</a> | <a href="https://swiftybeaver.com">Website</a> | <a href="https://twitter.com/SwiftyBeaver">Twitter</a> | <a href="#privacy">Privacy</a> | <a href="https://github.com/SwiftyBeaver/SwiftyBeaver/blob/master/LICENSE">License</a><br/></p>
|
||||
|
||||
<p align="center"><a href="https://swift.org" target="_blank"><img src="https://img.shields.io/badge/Language-Swift%203,%204%20&%205-orange.svg" alt="Language Swift 2, 3, 4 & 5"></a> <a href="https://circleci.com/gh/SwiftyBeaver/SwiftyBeaver" target="_blank"><img src="https://circleci.com/gh/SwiftyBeaver/SwiftyBeaver/tree/master.svg?style=shield" alt="CircleCI"/></a><br/><p>
|
||||
|
||||
----
|
||||
|
||||
<br/>
|
||||
|
||||
### During Development: Colored Logging to Xcode Console
|
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/564725/18608323/ac065a98-7ce6-11e6-8e1b-2a062d54a1d5.png" width="608">
|
||||
|
||||
[Learn more](http://docs.swiftybeaver.com/article/9-log-to-xcode-console) about colored logging to Xcode 8 Console with Swift 3, 4 & 5. For Swift 2.3 [use this Gist](https://gist.github.com/skreutzberger/7c396573796473ed1be2c6d15cafed34). **No need to hack Xcode 8 anymore** to get color. You can even customize the log level word (ATTENTION instead of ERROR maybe?), the general amount of displayed data and if you want to use the 💜s or replace them with something else 😉
|
||||
|
||||
<br/>
|
||||
|
||||
### During Development: Colored Logging to File
|
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/564725/18608325/b7ecd4c2-7ce6-11e6-829b-7f8f9fe6ef2f.png" width="738">
|
||||
|
||||
[Learn more](http://docs.swiftybeaver.com/article/10-log-to-file) about logging to file which is great for Terminal.app fans or to store logs on disk.
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
### On Release: Encrypted Logging to SwiftyBeaver Platform
|
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/564725/14281408/38d6a6ba-fb39-11e5-9584-34e3679bb1c5.jpg" width="700">
|
||||
|
||||
[Learn more](http://docs.swiftybeaver.com/article/11-log-to-swiftybeaver-platform) about logging to the SwiftyBeaver Platform **during release!**
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
### Browse, Search & Filter via Mac App
|
||||
|
||||

|
||||
|
||||
Conveniently access your logs during development & release with our [free Mac App](https://swiftybeaver.com).
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
### On Release: Enterprise-ready Logging to Your Elasticsearch & Kibana (on-premise)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/564725/34486363-dc501aec-efcf-11e7-92b2-1163cca9e7aa.jpg" width="700">
|
||||
|
||||
[Learn more](http://docs.swiftybeaver.com/article/34-enterprise-quick-start-via-docker) about **legally compliant**, end-to-end encrypted logging to Elasticsearch with **SwiftyBeaver Enterprise**. Install via Docker or manual, fully-featured free trial included!
|
||||
|
||||
<br/>
|
||||
|
||||
### Google Cloud & More
|
||||
|
||||
You can fully customize your log format, turn it into JSON, or create your own destinations. For example our [Google Cloud Destination](https://github.com/SwiftyBeaver/SwiftyBeaver/blob/master/Sources/GoogleCloudDestination.swift) is just another customized logging format which adds the powerful functionality of automatic server-side Swift logging when hosted on Google Cloud Platform.
|
||||
|
||||
<br/>
|
||||
|
||||
----
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Installation
|
||||
|
||||
- For **Swift 4 & 5** install the latest SwiftyBeaver version
|
||||
- For **Swift 3** install SwiftyBeaver 1.8.4
|
||||
- For **Swift 2** install SwiftyBeaver 0.7.0
|
||||
|
||||
<br/>
|
||||
|
||||
### Carthage
|
||||
|
||||
You can use [Carthage](https://github.com/Carthage/Carthage) to install SwiftyBeaver by adding that to your Cartfile:
|
||||
|
||||
Swift 4 & 5:
|
||||
``` Swift
|
||||
github "SwiftyBeaver/SwiftyBeaver"
|
||||
```
|
||||
|
||||
|
||||
Swift 3:
|
||||
``` Swift
|
||||
github "SwiftyBeaver/SwiftyBeaver" ~> 1.8.4
|
||||
```
|
||||
|
||||
Swift 2:
|
||||
``` Swift
|
||||
github "SwiftyBeaver/SwiftyBeaver" ~> 0.7
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
For [Swift Package Manager](https://swift.org/package-manager/) add the following package to your Package.swift file. Just Swift 4 & 5 are supported:
|
||||
|
||||
``` Swift
|
||||
.package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", .upToNextMajor(from: "1.9.0")),
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
### CocoaPods
|
||||
|
||||
To use [CocoaPods](https://cocoapods.org) just add this to your Podfile:
|
||||
|
||||
Swift 4 & 5:
|
||||
``` Swift
|
||||
pod 'SwiftyBeaver'
|
||||
```
|
||||
|
||||
Swift 3:
|
||||
``` Ruby
|
||||
target 'MyProject' do
|
||||
use_frameworks!
|
||||
|
||||
# Pods for MyProject
|
||||
pod 'SwiftyBeaver', '~> 1.8.4'
|
||||
end
|
||||
```
|
||||
|
||||
Swift 2:
|
||||
``` Ruby
|
||||
target 'MyProject' do
|
||||
use_frameworks!
|
||||
|
||||
# Pods for MyProject
|
||||
pod 'SwiftyBeaver', '~> 0.7'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.build_configurations.each do |config|
|
||||
# Configure Pod targets for Xcode 8 with Swift 2.3
|
||||
config.build_settings['SWIFT_VERSION'] = '2.3'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Usage
|
||||
|
||||
Add that near the top of your `AppDelegate.swift` to be able to use SwiftyBeaver in your whole project.
|
||||
|
||||
``` Swift
|
||||
import SwiftyBeaver
|
||||
let log = SwiftyBeaver.self
|
||||
|
||||
```
|
||||
|
||||
At the the beginning of your `AppDelegate:didFinishLaunchingWithOptions()` add the SwiftyBeaver log destinations (console, file, etc.), optionally adjust the [log format](http://docs.swiftybeaver.com//article/20-custom-format) and then you can already do the following log level calls globally:
|
||||
|
||||
``` Swift
|
||||
// add log destinations. at least one is needed!
|
||||
let console = ConsoleDestination() // log to Xcode Console
|
||||
let file = FileDestination() // log to default swiftybeaver.log file
|
||||
let cloud = SBPlatformDestination(appID: "foo", appSecret: "bar", encryptionKey: "123") // to cloud
|
||||
|
||||
// use custom format and set console output to short time, log level & message
|
||||
console.format = "$DHH:mm:ss$d $L $M"
|
||||
// or use this for JSON output: console.format = "$J"
|
||||
|
||||
// add the destinations to SwiftyBeaver
|
||||
log.addDestination(console)
|
||||
log.addDestination(file)
|
||||
log.addDestination(cloud)
|
||||
|
||||
// Now let’s log!
|
||||
log.verbose("not so important") // prio 1, VERBOSE in silver
|
||||
log.debug("something to debug") // prio 2, DEBUG in green
|
||||
log.info("a nice information") // prio 3, INFO in blue
|
||||
log.warning("oh no, that won’t be good") // prio 4, WARNING in yellow
|
||||
log.error("ouch, an error did occur!") // prio 5, ERROR in red
|
||||
|
||||
// log anything!
|
||||
log.verbose(123)
|
||||
log.info(-123.45678)
|
||||
log.warning(Date())
|
||||
log.error(["I", "like", "logs!"])
|
||||
log.error(["name": "Mr Beaver", "address": "7 Beaver Lodge"])
|
||||
|
||||
// optionally add context to a log message
|
||||
console.format = "$L: $M $X"
|
||||
log.debug("age", 123) // "DEBUG: age 123"
|
||||
log.info("my data", context: [1, "a", 2]) // "INFO: my data [1, \"a\", 2]"
|
||||
|
||||
```
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Server-side Swift
|
||||
|
||||
We ❤️ server-side Swift 4 & 5 and SwiftyBeaver supports it **out-of-the-box**! Try for yourself and run SwiftyBeaver inside a Ubuntu Docker container. Just install Docker and then go to your the project folder on macOS or Ubuntu and type:
|
||||
|
||||
```shell
|
||||
# create docker image, build SwiftyBeaver and run unit tests
|
||||
swift build --clean && docker build --rm -t swiftybeaver .
|
||||
|
||||
# optionally log into container to run Swift CLI and do more stuff
|
||||
docker run --rm -it --privileged=true -v $PWD:/app swiftybeaver
|
||||
```
|
||||
|
||||
Best: for the popular server-side Swift web framework [Vapor](https://github.com/vapor/vapor) you can use **[our Vapor logging provider](https://github.com/SwiftyBeaver/SwiftyBeaver-Vapor)** which makes server logging awesome again 🙌
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Documentation
|
||||
|
||||
**Getting Started:**
|
||||
|
||||
- [Features](http://docs.swiftybeaver.com/article/7-introduction)
|
||||
- [Installation](http://docs.swiftybeaver.com/article/5-installation)
|
||||
- [Basic Setup](http://docs.swiftybeaver.com/article/6-basic-setup)
|
||||
|
||||
**Logging Destinations:**
|
||||
|
||||
- [Colored Logging to Xcode Console](http://docs.swiftybeaver.com/article/9-log-to-xcode-console)
|
||||
- [Colored Logging to File](http://docs.swiftybeaver.com/article/10-log-to-file)
|
||||
- [Encrypted Logging & Analytics to SwiftyBeaver Platform](http://docs.swiftybeaver.com/article/11-log-to-swiftybeaver-platform)
|
||||
- [Encrypted Logging & Analytics to Elasticsearch & Kibana](http://docs.swiftybeaver.com/article/34-enterprise-quick-start-via-docker)
|
||||
|
||||
|
||||
**Advanced Topics:**
|
||||
|
||||
- [Custom Format & Context](http://docs.swiftybeaver.com/article/20-custom-format)
|
||||
- [Filters](http://docs.swiftybeaver.com/article/21-filters)
|
||||
|
||||
**Stay Informed:**
|
||||
|
||||
- [Official Website](https://swiftybeaver.com))
|
||||
- [On Twitter](https://twitter.com/SwiftyBeaver)
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Privacy
|
||||
|
||||
**SwiftyBeaver is not collecting any data without you as a developer knowing about it**. That's why it is **open-source** and developed in a simple way to be easy to inspect and check what it is actually doing under the hood.
|
||||
|
||||
The only sending to servers is done if you use the `SBPlatformDestination`. That destination is meant for production logging and on default it sends your logs plus additional device information **end-to-end encrypted** to our cloud service. Our cloud service **can not decrypt the data**.
|
||||
|
||||
Instead, you install our Mac App and that Mac App downloads the encrypted logs from the cloud and decrypts and shows them to you. Additionally, the Mac App stores all data that it downloads in a local SQLite database file on your computer so that you actually "physically" own your data.
|
||||
|
||||
The business model of the SwiftyBeaver cloud service is to provide the most secure logging solution in the market. On purpose we do not provide a web UI for you because it would require us to store your encryption key on our servers.
|
||||
|
||||
**Only you can see the logging and device data** which is sent from your users' devices. Our servers just see encrypted data and do not know your decryption key.
|
||||
|
||||
SwiftyBeaver is **fully GDPR compliant** due to its focus on encryption and transparency in what data is collected and also meets **Apple’s latest requirements** on the privacy of 3rd party frameworks.
|
||||
|
||||
Our Enterprise offering is an even more secure solution where you are not using anymore our cloud service and Mac App but you send your end-to-end encrypted logs directly to your own servers and you store them in your Elasticsearch cluster. The **Enterprise offering is used by health tech** and governmental institutions which require the highest level of privacy and security.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## End-to-End Encryption
|
||||
|
||||
SwiftyBeaver is using symmetric AES256CBC encryption in the `SBPlatformDestination` destination. No other officially supported destination uses encryption.
|
||||
|
||||
The encryption used in the `SBPlatformDestination` destination is end-to-end. The open-source SwiftyBeaver logging framework symmetrically encrypts all logging data on your client's device inside your app (iPhone, iPad, ...) before it is sent to the SwiftyBeaver Crypto Cloud. The decryption is done on your Mac which has the SwiftyBeaver Mac App installed. All logging data stays encrypted in the SwiftyBeaver Crypto Cloud due to the lack of the password.
|
||||
|
||||
You are using the encryption at your own risk. SwiftyBeaver’s authors and contributors do not take over any guarantee about the absence of potential security or cryptopgraphy issues, weaknesses, etc.; please also read the LICENSE file for details. Also if you are interested in cryptography in general, please have a look at the file AES256CBC.swift to learn more about the cryptographical implementation.
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## License
|
||||
|
||||
SwiftyBeaver Framework is released under the [MIT License](https://github.com/SwiftyBeaver/SwiftyBeaver/blob/master/LICENSE).
|
||||
@@ -0,0 +1,788 @@
|
||||
//
|
||||
// AES256CBC.swift
|
||||
// AES256CBC https://github.com/SwiftyBeaver/AES256CBC
|
||||
//
|
||||
// Created by Sebastian Kreutzberger on 2/9/16.
|
||||
// Copyright © 2016 SwiftyBeaver. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class AES256CBC {
|
||||
|
||||
/// returns optional encrypted string via AES-256CBC
|
||||
/// automatically generates and puts a random IV at first 16 chars
|
||||
/// the password must be exactly 32 chars long for AES-256
|
||||
class func encryptString(_ str: String, password: String) -> String? {
|
||||
if !str.isEmpty && Data(password.utf8).count == 32 {
|
||||
let iv = randomText(16)
|
||||
let key = password
|
||||
|
||||
guard let encryptedString = try? aesEncrypt(str, key: key, iv: iv) else {
|
||||
print("an error occured while encrypting")
|
||||
return nil
|
||||
}
|
||||
return iv + encryptedString
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// returns optional decrypted string via AES-256CBC
|
||||
/// IV need to be at first 16 chars, password must be 32 chars long
|
||||
class func decryptString(_ str: String, password: String) -> String? {
|
||||
if Data(str.utf8).count > 16 && Data(password.utf8).count == 32 {
|
||||
// get AES initialization vector from first 16 chars
|
||||
let iv = String(str.prefix(16))
|
||||
let encryptedString = str.replacingOccurrences(of: iv, with: "",
|
||||
options: String.CompareOptions.literal,
|
||||
range: nil) // remove IV
|
||||
|
||||
guard let decryptedString = try? aesDecrypt(encryptedString, key: password, iv: iv) else {
|
||||
print("an error occured while decrypting")
|
||||
return nil
|
||||
}
|
||||
return decryptedString
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// returns random string (uppercase & lowercase, no spaces) of 32 characters length
|
||||
/// which can be used as SHA-256 compatbile password
|
||||
class func generatePassword() -> String {
|
||||
return randomText(32)
|
||||
}
|
||||
|
||||
/// returns random text of a defined length
|
||||
public class func randomText(_ length: Int) -> String {
|
||||
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return String((0..<length).map { _ in letters.randomElement()!})
|
||||
}
|
||||
|
||||
/// returns encrypted string, IV must be 16 chars long
|
||||
fileprivate class func aesEncrypt(_ str: String, key: String, iv: String) throws -> String {
|
||||
let keyData = key.data(using: String.Encoding.utf8)!
|
||||
let ivData = iv.data(using: String.Encoding.utf8)!
|
||||
let data = str.data(using: String.Encoding.utf8)!
|
||||
#if swift(>=5)
|
||||
let enc = try Data(AESCipher(key: keyData.bytes,
|
||||
iv: ivData.bytes).encrypt(bytes: data.bytes))
|
||||
#else
|
||||
let enc = try Data(bytes: AESCipher(key: keyData.bytes,
|
||||
iv: ivData.bytes).encrypt(bytes: data.bytes))
|
||||
#endif
|
||||
return enc.base64EncodedString(options: [])
|
||||
}
|
||||
|
||||
/// returns decrypted string, IV must be 16 chars long
|
||||
fileprivate class func aesDecrypt(_ str: String, key: String, iv: String) throws -> String {
|
||||
let keyData = key.data(using: String.Encoding.utf8)!
|
||||
let ivData = iv.data(using: String.Encoding.utf8)!
|
||||
if let data = Data(base64Encoded: str) {
|
||||
#if swift(>=5)
|
||||
let dec = try Data(AESCipher(key: keyData.bytes,
|
||||
iv: ivData.bytes).decrypt(bytes: data.bytes))
|
||||
#else
|
||||
let dec = try Data(bytes: AESCipher(key: keyData.bytes,
|
||||
iv: ivData.bytes).decrypt(bytes: data.bytes))
|
||||
#endif
|
||||
guard let decryptStr = String(data: dec, encoding: String.Encoding.utf8) else {
|
||||
throw NSError(domain: "Invalid utf8 data", code: 0, userInfo: nil)
|
||||
}
|
||||
return decryptStr
|
||||
} else {
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
// The following is an altered source version that only includes AES-CBC. The original software can be found at:
|
||||
// https://github.com/krzyzanowskim/CryptoSwift
|
||||
//
|
||||
// This is the original copyright notice:
|
||||
//
|
||||
// Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
|
||||
// This software is provided 'as-is', without any express or implied warranty.
|
||||
//
|
||||
// In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
//
|
||||
// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
|
||||
// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
// - This notice may not be removed or altered from any source or binary distribution.
|
||||
//
|
||||
// swiftlint:enable line_length
|
||||
|
||||
// MARK: - AESCipher
|
||||
|
||||
private typealias Key = Array<UInt8>
|
||||
|
||||
final private class AESCipher {
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case blockSizeExceeded
|
||||
case dataPaddingRequired
|
||||
case invalidKeyOrInitializationVector
|
||||
case invalidInitializationVector
|
||||
}
|
||||
|
||||
enum Variant: Int {
|
||||
case aes128 = 1, aes192, aes256
|
||||
|
||||
var Nk: Int { // Nk words
|
||||
return [4, 6, 8][self.rawValue - 1]
|
||||
}
|
||||
|
||||
var Nb: Int { // Nb words
|
||||
return 4
|
||||
}
|
||||
|
||||
var Nr: Int { // Nr
|
||||
return Nk + 6
|
||||
}
|
||||
}
|
||||
|
||||
static let blockSize: Int = 16 // 128 /8
|
||||
|
||||
var variant: Variant {
|
||||
switch self.key.count * 8 {
|
||||
case 128:
|
||||
return .aes128
|
||||
case 192:
|
||||
return .aes192
|
||||
case 256:
|
||||
return .aes256
|
||||
default:
|
||||
preconditionFailure("Unknown AES variant for given key.")
|
||||
}
|
||||
}
|
||||
private let key: [UInt8]
|
||||
private let iv: [UInt8]?
|
||||
private let blockMode = CBCBlockMode()
|
||||
private lazy var expandedKey: Array<Array<UInt32>> = self.expandKey(self.key, variant: self.variant)
|
||||
private lazy var expandedKeyInv: Array<Array<UInt32>> = self.expandKeyInv(self.key, variant: self.variant)
|
||||
|
||||
private lazy var sBoxes:(sBox: [UInt32], invSBox: [UInt32]) = self.calculateSBox()
|
||||
private lazy var sBox: [UInt32] = self.sBoxes.sBox
|
||||
private lazy var sBoxInv: [UInt32] = self.sBoxes.invSBox
|
||||
|
||||
// Parameters for Linear Congruence Generators
|
||||
private let Rcon: [UInt8] = [
|
||||
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d]
|
||||
|
||||
// swiftlint:disable line_length
|
||||
fileprivate let T0: Array<UInt32> = [0xa56363c6, 0x847c7cf8, 0x997777ee, 0x8d7b7bf6, 0xdf2f2ff, 0xbd6b6bd6, 0xb16f6fde, 0x54c5c591, 0x50303060, 0x3010102, 0xa96767ce, 0x7d2b2b56, 0x19fefee7, 0x62d7d7b5, 0xe6abab4d, 0x9a7676ec, 0x45caca8f, 0x9d82821f, 0x40c9c989, 0x877d7dfa, 0x15fafaef, 0xeb5959b2, 0xc947478e, 0xbf0f0fb, 0xecadad41, 0x67d4d4b3, 0xfda2a25f, 0xeaafaf45, 0xbf9c9c23, 0xf7a4a453, 0x967272e4, 0x5bc0c09b, 0xc2b7b775, 0x1cfdfde1, 0xae93933d, 0x6a26264c, 0x5a36366c, 0x413f3f7e, 0x2f7f7f5, 0x4fcccc83, 0x5c343468, 0xf4a5a551, 0x34e5e5d1, 0x8f1f1f9, 0x937171e2, 0x73d8d8ab, 0x53313162, 0x3f15152a, 0xc040408, 0x52c7c795, 0x65232346, 0x5ec3c39d, 0x28181830, 0xa1969637, 0xf05050a, 0xb59a9a2f, 0x907070e, 0x36121224, 0x9b80801b, 0x3de2e2df, 0x26ebebcd, 0x6927274e, 0xcdb2b27f, 0x9f7575ea, 0x1b090912, 0x9e83831d, 0x742c2c58, 0x2e1a1a34, 0x2d1b1b36, 0xb26e6edc, 0xee5a5ab4, 0xfba0a05b, 0xf65252a4, 0x4d3b3b76, 0x61d6d6b7, 0xceb3b37d, 0x7b292952, 0x3ee3e3dd, 0x712f2f5e, 0x97848413, 0xf55353a6, 0x68d1d1b9, 0x0, 0x2cededc1, 0x60202040, 0x1ffcfce3, 0xc8b1b179, 0xed5b5bb6, 0xbe6a6ad4, 0x46cbcb8d, 0xd9bebe67, 0x4b393972, 0xde4a4a94, 0xd44c4c98, 0xe85858b0, 0x4acfcf85, 0x6bd0d0bb, 0x2aefefc5, 0xe5aaaa4f, 0x16fbfbed, 0xc5434386, 0xd74d4d9a, 0x55333366, 0x94858511, 0xcf45458a, 0x10f9f9e9, 0x6020204, 0x817f7ffe, 0xf05050a0, 0x443c3c78, 0xba9f9f25, 0xe3a8a84b, 0xf35151a2, 0xfea3a35d, 0xc0404080, 0x8a8f8f05, 0xad92923f, 0xbc9d9d21, 0x48383870, 0x4f5f5f1, 0xdfbcbc63, 0xc1b6b677, 0x75dadaaf, 0x63212142, 0x30101020, 0x1affffe5, 0xef3f3fd, 0x6dd2d2bf, 0x4ccdcd81, 0x140c0c18, 0x35131326, 0x2fececc3, 0xe15f5fbe, 0xa2979735, 0xcc444488, 0x3917172e, 0x57c4c493, 0xf2a7a755, 0x827e7efc, 0x473d3d7a, 0xac6464c8, 0xe75d5dba, 0x2b191932, 0x957373e6, 0xa06060c0, 0x98818119, 0xd14f4f9e, 0x7fdcdca3, 0x66222244, 0x7e2a2a54, 0xab90903b, 0x8388880b, 0xca46468c, 0x29eeeec7, 0xd3b8b86b, 0x3c141428, 0x79dedea7, 0xe25e5ebc, 0x1d0b0b16, 0x76dbdbad, 0x3be0e0db, 0x56323264, 0x4e3a3a74, 0x1e0a0a14, 0xdb494992, 0xa06060c, 0x6c242448, 0xe45c5cb8, 0x5dc2c29f, 0x6ed3d3bd, 0xefacac43, 0xa66262c4, 0xa8919139, 0xa4959531, 0x37e4e4d3, 0x8b7979f2, 0x32e7e7d5, 0x43c8c88b, 0x5937376e, 0xb76d6dda, 0x8c8d8d01, 0x64d5d5b1, 0xd24e4e9c, 0xe0a9a949, 0xb46c6cd8, 0xfa5656ac, 0x7f4f4f3, 0x25eaeacf, 0xaf6565ca, 0x8e7a7af4, 0xe9aeae47, 0x18080810, 0xd5baba6f, 0x887878f0, 0x6f25254a, 0x722e2e5c, 0x241c1c38, 0xf1a6a657, 0xc7b4b473, 0x51c6c697, 0x23e8e8cb, 0x7cdddda1, 0x9c7474e8, 0x211f1f3e, 0xdd4b4b96, 0xdcbdbd61, 0x868b8b0d, 0x858a8a0f, 0x907070e0, 0x423e3e7c, 0xc4b5b571, 0xaa6666cc, 0xd8484890, 0x5030306, 0x1f6f6f7, 0x120e0e1c, 0xa36161c2, 0x5f35356a, 0xf95757ae, 0xd0b9b969, 0x91868617, 0x58c1c199, 0x271d1d3a, 0xb99e9e27, 0x38e1e1d9, 0x13f8f8eb, 0xb398982b, 0x33111122, 0xbb6969d2, 0x70d9d9a9, 0x898e8e07, 0xa7949433, 0xb69b9b2d, 0x221e1e3c, 0x92878715, 0x20e9e9c9, 0x49cece87, 0xff5555aa, 0x78282850, 0x7adfdfa5, 0x8f8c8c03, 0xf8a1a159, 0x80898909, 0x170d0d1a, 0xdabfbf65, 0x31e6e6d7, 0xc6424284, 0xb86868d0, 0xc3414182, 0xb0999929, 0x772d2d5a, 0x110f0f1e, 0xcbb0b07b, 0xfc5454a8, 0xd6bbbb6d, 0x3a16162c]
|
||||
fileprivate let T0_INV: Array<UInt32> = [0x50a7f451, 0x5365417e, 0xc3a4171a, 0x965e273a, 0xcb6bab3b, 0xf1459d1f, 0xab58faac, 0x9303e34b, 0x55fa3020, 0xf66d76ad, 0x9176cc88, 0x254c02f5, 0xfcd7e54f, 0xd7cb2ac5, 0x80443526, 0x8fa362b5, 0x495ab1de, 0x671bba25, 0x980eea45, 0xe1c0fe5d, 0x2752fc3, 0x12f04c81, 0xa397468d, 0xc6f9d36b, 0xe75f8f03, 0x959c9215, 0xeb7a6dbf, 0xda595295, 0x2d83bed4, 0xd3217458, 0x2969e049, 0x44c8c98e, 0x6a89c275, 0x78798ef4, 0x6b3e5899, 0xdd71b927, 0xb64fe1be, 0x17ad88f0, 0x66ac20c9, 0xb43ace7d, 0x184adf63, 0x82311ae5, 0x60335197, 0x457f5362, 0xe07764b1, 0x84ae6bbb, 0x1ca081fe, 0x942b08f9, 0x58684870, 0x19fd458f, 0x876cde94, 0xb7f87b52, 0x23d373ab, 0xe2024b72, 0x578f1fe3, 0x2aab5566, 0x728ebb2, 0x3c2b52f, 0x9a7bc586, 0xa50837d3, 0xf2872830, 0xb2a5bf23, 0xba6a0302, 0x5c8216ed, 0x2b1ccf8a, 0x92b479a7, 0xf0f207f3, 0xa1e2694e, 0xcdf4da65, 0xd5be0506, 0x1f6234d1, 0x8afea6c4, 0x9d532e34, 0xa055f3a2, 0x32e18a05, 0x75ebf6a4, 0x39ec830b, 0xaaef6040, 0x69f715e, 0x51106ebd, 0xf98a213e, 0x3d06dd96, 0xae053edd, 0x46bde64d, 0xb58d5491, 0x55dc471, 0x6fd40604, 0xff155060, 0x24fb9819, 0x97e9bdd6, 0xcc434089, 0x779ed967, 0xbd42e8b0, 0x888b8907, 0x385b19e7, 0xdbeec879, 0x470a7ca1, 0xe90f427c, 0xc91e84f8, 0x0, 0x83868009, 0x48ed2b32, 0xac70111e, 0x4e725a6c, 0xfbff0efd, 0x5638850f, 0x1ed5ae3d, 0x27392d36, 0x64d90f0a, 0x21a65c68, 0xd1545b9b, 0x3a2e3624, 0xb1670a0c, 0xfe75793, 0xd296eeb4, 0x9e919b1b, 0x4fc5c080, 0xa220dc61, 0x694b775a, 0x161a121c, 0xaba93e2, 0xe52aa0c0, 0x43e0223c, 0x1d171b12, 0xb0d090e, 0xadc78bf2, 0xb9a8b62d, 0xc8a91e14, 0x8519f157, 0x4c0775af, 0xbbdd99ee, 0xfd607fa3, 0x9f2601f7, 0xbcf5725c, 0xc53b6644, 0x347efb5b, 0x7629438b, 0xdcc623cb, 0x68fcedb6, 0x63f1e4b8, 0xcadc31d7, 0x10856342, 0x40229713, 0x2011c684, 0x7d244a85, 0xf83dbbd2, 0x1132f9ae, 0x6da129c7, 0x4b2f9e1d, 0xf330b2dc, 0xec52860d, 0xd0e3c177, 0x6c16b32b, 0x99b970a9, 0xfa489411, 0x2264e947, 0xc48cfca8, 0x1a3ff0a0, 0xd82c7d56, 0xef903322, 0xc74e4987, 0xc1d138d9, 0xfea2ca8c, 0x360bd498, 0xcf81f5a6, 0x28de7aa5, 0x268eb7da, 0xa4bfad3f, 0xe49d3a2c, 0xd927850, 0x9bcc5f6a, 0x62467e54, 0xc2138df6, 0xe8b8d890, 0x5ef7392e, 0xf5afc382, 0xbe805d9f, 0x7c93d069, 0xa92dd56f, 0xb31225cf, 0x3b99acc8, 0xa77d1810, 0x6e639ce8, 0x7bbb3bdb, 0x97826cd, 0xf418596e, 0x1b79aec, 0xa89a4f83, 0x656e95e6, 0x7ee6ffaa, 0x8cfbc21, 0xe6e815ef, 0xd99be7ba, 0xce366f4a, 0xd4099fea, 0xd67cb029, 0xafb2a431, 0x31233f2a, 0x3094a5c6, 0xc066a235, 0x37bc4e74, 0xa6ca82fc, 0xb0d090e0, 0x15d8a733, 0x4a9804f1, 0xf7daec41, 0xe50cd7f, 0x2ff69117, 0x8dd64d76, 0x4db0ef43, 0x544daacc, 0xdf0496e4, 0xe3b5d19e, 0x1b886a4c, 0xb81f2cc1, 0x7f516546, 0x4ea5e9d, 0x5d358c01, 0x737487fa, 0x2e410bfb, 0x5a1d67b3, 0x52d2db92, 0x335610e9, 0x1347d66d, 0x8c61d79a, 0x7a0ca137, 0x8e14f859, 0x893c13eb, 0xee27a9ce, 0x35c961b7, 0xede51ce1, 0x3cb1477a, 0x59dfd29c, 0x3f73f255, 0x79ce1418, 0xbf37c773, 0xeacdf753, 0x5baafd5f, 0x146f3ddf, 0x86db4478, 0x81f3afca, 0x3ec468b9, 0x2c342438, 0x5f40a3c2, 0x72c31d16, 0xc25e2bc, 0x8b493c28, 0x41950dff, 0x7101a839, 0xdeb30c08, 0x9ce4b4d8, 0x90c15664, 0x6184cb7b, 0x70b632d5, 0x745c6c48, 0x4257b8d0]
|
||||
fileprivate let T1: Array<UInt32> = [0x6363c6a5, 0x7c7cf884, 0x7777ee99, 0x7b7bf68d, 0xf2f2ff0d, 0x6b6bd6bd, 0x6f6fdeb1, 0xc5c59154, 0x30306050, 0x1010203, 0x6767cea9, 0x2b2b567d, 0xfefee719, 0xd7d7b562, 0xabab4de6, 0x7676ec9a, 0xcaca8f45, 0x82821f9d, 0xc9c98940, 0x7d7dfa87, 0xfafaef15, 0x5959b2eb, 0x47478ec9, 0xf0f0fb0b, 0xadad41ec, 0xd4d4b367, 0xa2a25ffd, 0xafaf45ea, 0x9c9c23bf, 0xa4a453f7, 0x7272e496, 0xc0c09b5b, 0xb7b775c2, 0xfdfde11c, 0x93933dae, 0x26264c6a, 0x36366c5a, 0x3f3f7e41, 0xf7f7f502, 0xcccc834f, 0x3434685c, 0xa5a551f4, 0xe5e5d134, 0xf1f1f908, 0x7171e293, 0xd8d8ab73, 0x31316253, 0x15152a3f, 0x404080c, 0xc7c79552, 0x23234665, 0xc3c39d5e, 0x18183028, 0x969637a1, 0x5050a0f, 0x9a9a2fb5, 0x7070e09, 0x12122436, 0x80801b9b, 0xe2e2df3d, 0xebebcd26, 0x27274e69, 0xb2b27fcd, 0x7575ea9f, 0x909121b, 0x83831d9e, 0x2c2c5874, 0x1a1a342e, 0x1b1b362d, 0x6e6edcb2, 0x5a5ab4ee, 0xa0a05bfb, 0x5252a4f6, 0x3b3b764d, 0xd6d6b761, 0xb3b37dce, 0x2929527b, 0xe3e3dd3e, 0x2f2f5e71, 0x84841397, 0x5353a6f5, 0xd1d1b968, 0x0, 0xededc12c, 0x20204060, 0xfcfce31f, 0xb1b179c8, 0x5b5bb6ed, 0x6a6ad4be, 0xcbcb8d46, 0xbebe67d9, 0x3939724b, 0x4a4a94de, 0x4c4c98d4, 0x5858b0e8, 0xcfcf854a, 0xd0d0bb6b, 0xefefc52a, 0xaaaa4fe5, 0xfbfbed16, 0x434386c5, 0x4d4d9ad7, 0x33336655, 0x85851194, 0x45458acf, 0xf9f9e910, 0x2020406, 0x7f7ffe81, 0x5050a0f0, 0x3c3c7844, 0x9f9f25ba, 0xa8a84be3, 0x5151a2f3, 0xa3a35dfe, 0x404080c0, 0x8f8f058a, 0x92923fad, 0x9d9d21bc, 0x38387048, 0xf5f5f104, 0xbcbc63df, 0xb6b677c1, 0xdadaaf75, 0x21214263, 0x10102030, 0xffffe51a, 0xf3f3fd0e, 0xd2d2bf6d, 0xcdcd814c, 0xc0c1814, 0x13132635, 0xececc32f, 0x5f5fbee1, 0x979735a2, 0x444488cc, 0x17172e39, 0xc4c49357, 0xa7a755f2, 0x7e7efc82, 0x3d3d7a47, 0x6464c8ac, 0x5d5dbae7, 0x1919322b, 0x7373e695, 0x6060c0a0, 0x81811998, 0x4f4f9ed1, 0xdcdca37f, 0x22224466, 0x2a2a547e, 0x90903bab, 0x88880b83, 0x46468cca, 0xeeeec729, 0xb8b86bd3, 0x1414283c, 0xdedea779, 0x5e5ebce2, 0xb0b161d, 0xdbdbad76, 0xe0e0db3b, 0x32326456, 0x3a3a744e, 0xa0a141e, 0x494992db, 0x6060c0a, 0x2424486c, 0x5c5cb8e4, 0xc2c29f5d, 0xd3d3bd6e, 0xacac43ef, 0x6262c4a6, 0x919139a8, 0x959531a4, 0xe4e4d337, 0x7979f28b, 0xe7e7d532, 0xc8c88b43, 0x37376e59, 0x6d6ddab7, 0x8d8d018c, 0xd5d5b164, 0x4e4e9cd2, 0xa9a949e0, 0x6c6cd8b4, 0x5656acfa, 0xf4f4f307, 0xeaeacf25, 0x6565caaf, 0x7a7af48e, 0xaeae47e9, 0x8081018, 0xbaba6fd5, 0x7878f088, 0x25254a6f, 0x2e2e5c72, 0x1c1c3824, 0xa6a657f1, 0xb4b473c7, 0xc6c69751, 0xe8e8cb23, 0xdddda17c, 0x7474e89c, 0x1f1f3e21, 0x4b4b96dd, 0xbdbd61dc, 0x8b8b0d86, 0x8a8a0f85, 0x7070e090, 0x3e3e7c42, 0xb5b571c4, 0x6666ccaa, 0x484890d8, 0x3030605, 0xf6f6f701, 0xe0e1c12, 0x6161c2a3, 0x35356a5f, 0x5757aef9, 0xb9b969d0, 0x86861791, 0xc1c19958, 0x1d1d3a27, 0x9e9e27b9, 0xe1e1d938, 0xf8f8eb13, 0x98982bb3, 0x11112233, 0x6969d2bb, 0xd9d9a970, 0x8e8e0789, 0x949433a7, 0x9b9b2db6, 0x1e1e3c22, 0x87871592, 0xe9e9c920, 0xcece8749, 0x5555aaff, 0x28285078, 0xdfdfa57a, 0x8c8c038f, 0xa1a159f8, 0x89890980, 0xd0d1a17, 0xbfbf65da, 0xe6e6d731, 0x424284c6, 0x6868d0b8, 0x414182c3, 0x999929b0, 0x2d2d5a77, 0xf0f1e11, 0xb0b07bcb, 0x5454a8fc, 0xbbbb6dd6, 0x16162c3a]
|
||||
fileprivate let T1_INV: Array<UInt32> = [0xa7f45150, 0x65417e53, 0xa4171ac3, 0x5e273a96, 0x6bab3bcb, 0x459d1ff1, 0x58faacab, 0x3e34b93, 0xfa302055, 0x6d76adf6, 0x76cc8891, 0x4c02f525, 0xd7e54ffc, 0xcb2ac5d7, 0x44352680, 0xa362b58f, 0x5ab1de49, 0x1bba2567, 0xeea4598, 0xc0fe5de1, 0x752fc302, 0xf04c8112, 0x97468da3, 0xf9d36bc6, 0x5f8f03e7, 0x9c921595, 0x7a6dbfeb, 0x595295da, 0x83bed42d, 0x217458d3, 0x69e04929, 0xc8c98e44, 0x89c2756a, 0x798ef478, 0x3e58996b, 0x71b927dd, 0x4fe1beb6, 0xad88f017, 0xac20c966, 0x3ace7db4, 0x4adf6318, 0x311ae582, 0x33519760, 0x7f536245, 0x7764b1e0, 0xae6bbb84, 0xa081fe1c, 0x2b08f994, 0x68487058, 0xfd458f19, 0x6cde9487, 0xf87b52b7, 0xd373ab23, 0x24b72e2, 0x8f1fe357, 0xab55662a, 0x28ebb207, 0xc2b52f03, 0x7bc5869a, 0x837d3a5, 0x872830f2, 0xa5bf23b2, 0x6a0302ba, 0x8216ed5c, 0x1ccf8a2b, 0xb479a792, 0xf207f3f0, 0xe2694ea1, 0xf4da65cd, 0xbe0506d5, 0x6234d11f, 0xfea6c48a, 0x532e349d, 0x55f3a2a0, 0xe18a0532, 0xebf6a475, 0xec830b39, 0xef6040aa, 0x9f715e06, 0x106ebd51, 0x8a213ef9, 0x6dd963d, 0x53eddae, 0xbde64d46, 0x8d5491b5, 0x5dc47105, 0xd406046f, 0x155060ff, 0xfb981924, 0xe9bdd697, 0x434089cc, 0x9ed96777, 0x42e8b0bd, 0x8b890788, 0x5b19e738, 0xeec879db, 0xa7ca147, 0xf427ce9, 0x1e84f8c9, 0x0, 0x86800983, 0xed2b3248, 0x70111eac, 0x725a6c4e, 0xff0efdfb, 0x38850f56, 0xd5ae3d1e, 0x392d3627, 0xd90f0a64, 0xa65c6821, 0x545b9bd1, 0x2e36243a, 0x670a0cb1, 0xe757930f, 0x96eeb4d2, 0x919b1b9e, 0xc5c0804f, 0x20dc61a2, 0x4b775a69, 0x1a121c16, 0xba93e20a, 0x2aa0c0e5, 0xe0223c43, 0x171b121d, 0xd090e0b, 0xc78bf2ad, 0xa8b62db9, 0xa91e14c8, 0x19f15785, 0x775af4c, 0xdd99eebb, 0x607fa3fd, 0x2601f79f, 0xf5725cbc, 0x3b6644c5, 0x7efb5b34, 0x29438b76, 0xc623cbdc, 0xfcedb668, 0xf1e4b863, 0xdc31d7ca, 0x85634210, 0x22971340, 0x11c68420, 0x244a857d, 0x3dbbd2f8, 0x32f9ae11, 0xa129c76d, 0x2f9e1d4b, 0x30b2dcf3, 0x52860dec, 0xe3c177d0, 0x16b32b6c, 0xb970a999, 0x489411fa, 0x64e94722, 0x8cfca8c4, 0x3ff0a01a, 0x2c7d56d8, 0x903322ef, 0x4e4987c7, 0xd138d9c1, 0xa2ca8cfe, 0xbd49836, 0x81f5a6cf, 0xde7aa528, 0x8eb7da26, 0xbfad3fa4, 0x9d3a2ce4, 0x9278500d, 0xcc5f6a9b, 0x467e5462, 0x138df6c2, 0xb8d890e8, 0xf7392e5e, 0xafc382f5, 0x805d9fbe, 0x93d0697c, 0x2dd56fa9, 0x1225cfb3, 0x99acc83b, 0x7d1810a7, 0x639ce86e, 0xbb3bdb7b, 0x7826cd09, 0x18596ef4, 0xb79aec01, 0x9a4f83a8, 0x6e95e665, 0xe6ffaa7e, 0xcfbc2108, 0xe815efe6, 0x9be7bad9, 0x366f4ace, 0x99fead4, 0x7cb029d6, 0xb2a431af, 0x233f2a31, 0x94a5c630, 0x66a235c0, 0xbc4e7437, 0xca82fca6, 0xd090e0b0, 0xd8a73315, 0x9804f14a, 0xdaec41f7, 0x50cd7f0e, 0xf691172f, 0xd64d768d, 0xb0ef434d, 0x4daacc54, 0x496e4df, 0xb5d19ee3, 0x886a4c1b, 0x1f2cc1b8, 0x5165467f, 0xea5e9d04, 0x358c015d, 0x7487fa73, 0x410bfb2e, 0x1d67b35a, 0xd2db9252, 0x5610e933, 0x47d66d13, 0x61d79a8c, 0xca1377a, 0x14f8598e, 0x3c13eb89, 0x27a9ceee, 0xc961b735, 0xe51ce1ed, 0xb1477a3c, 0xdfd29c59, 0x73f2553f, 0xce141879, 0x37c773bf, 0xcdf753ea, 0xaafd5f5b, 0x6f3ddf14, 0xdb447886, 0xf3afca81, 0xc468b93e, 0x3424382c, 0x40a3c25f, 0xc31d1672, 0x25e2bc0c, 0x493c288b, 0x950dff41, 0x1a83971, 0xb30c08de, 0xe4b4d89c, 0xc1566490, 0x84cb7b61, 0xb632d570, 0x5c6c4874, 0x57b8d042]
|
||||
fileprivate let T2: Array<UInt32> = [0x63c6a563, 0x7cf8847c, 0x77ee9977, 0x7bf68d7b, 0xf2ff0df2, 0x6bd6bd6b, 0x6fdeb16f, 0xc59154c5, 0x30605030, 0x1020301, 0x67cea967, 0x2b567d2b, 0xfee719fe, 0xd7b562d7, 0xab4de6ab, 0x76ec9a76, 0xca8f45ca, 0x821f9d82, 0xc98940c9, 0x7dfa877d, 0xfaef15fa, 0x59b2eb59, 0x478ec947, 0xf0fb0bf0, 0xad41ecad, 0xd4b367d4, 0xa25ffda2, 0xaf45eaaf, 0x9c23bf9c, 0xa453f7a4, 0x72e49672, 0xc09b5bc0, 0xb775c2b7, 0xfde11cfd, 0x933dae93, 0x264c6a26, 0x366c5a36, 0x3f7e413f, 0xf7f502f7, 0xcc834fcc, 0x34685c34, 0xa551f4a5, 0xe5d134e5, 0xf1f908f1, 0x71e29371, 0xd8ab73d8, 0x31625331, 0x152a3f15, 0x4080c04, 0xc79552c7, 0x23466523, 0xc39d5ec3, 0x18302818, 0x9637a196, 0x50a0f05, 0x9a2fb59a, 0x70e0907, 0x12243612, 0x801b9b80, 0xe2df3de2, 0xebcd26eb, 0x274e6927, 0xb27fcdb2, 0x75ea9f75, 0x9121b09, 0x831d9e83, 0x2c58742c, 0x1a342e1a, 0x1b362d1b, 0x6edcb26e, 0x5ab4ee5a, 0xa05bfba0, 0x52a4f652, 0x3b764d3b, 0xd6b761d6, 0xb37dceb3, 0x29527b29, 0xe3dd3ee3, 0x2f5e712f, 0x84139784, 0x53a6f553, 0xd1b968d1, 0x0, 0xedc12ced, 0x20406020, 0xfce31ffc, 0xb179c8b1, 0x5bb6ed5b, 0x6ad4be6a, 0xcb8d46cb, 0xbe67d9be, 0x39724b39, 0x4a94de4a, 0x4c98d44c, 0x58b0e858, 0xcf854acf, 0xd0bb6bd0, 0xefc52aef, 0xaa4fe5aa, 0xfbed16fb, 0x4386c543, 0x4d9ad74d, 0x33665533, 0x85119485, 0x458acf45, 0xf9e910f9, 0x2040602, 0x7ffe817f, 0x50a0f050, 0x3c78443c, 0x9f25ba9f, 0xa84be3a8, 0x51a2f351, 0xa35dfea3, 0x4080c040, 0x8f058a8f, 0x923fad92, 0x9d21bc9d, 0x38704838, 0xf5f104f5, 0xbc63dfbc, 0xb677c1b6, 0xdaaf75da, 0x21426321, 0x10203010, 0xffe51aff, 0xf3fd0ef3, 0xd2bf6dd2, 0xcd814ccd, 0xc18140c, 0x13263513, 0xecc32fec, 0x5fbee15f, 0x9735a297, 0x4488cc44, 0x172e3917, 0xc49357c4, 0xa755f2a7, 0x7efc827e, 0x3d7a473d, 0x64c8ac64, 0x5dbae75d, 0x19322b19, 0x73e69573, 0x60c0a060, 0x81199881, 0x4f9ed14f, 0xdca37fdc, 0x22446622, 0x2a547e2a, 0x903bab90, 0x880b8388, 0x468cca46, 0xeec729ee, 0xb86bd3b8, 0x14283c14, 0xdea779de, 0x5ebce25e, 0xb161d0b, 0xdbad76db, 0xe0db3be0, 0x32645632, 0x3a744e3a, 0xa141e0a, 0x4992db49, 0x60c0a06, 0x24486c24, 0x5cb8e45c, 0xc29f5dc2, 0xd3bd6ed3, 0xac43efac, 0x62c4a662, 0x9139a891, 0x9531a495, 0xe4d337e4, 0x79f28b79, 0xe7d532e7, 0xc88b43c8, 0x376e5937, 0x6ddab76d, 0x8d018c8d, 0xd5b164d5, 0x4e9cd24e, 0xa949e0a9, 0x6cd8b46c, 0x56acfa56, 0xf4f307f4, 0xeacf25ea, 0x65caaf65, 0x7af48e7a, 0xae47e9ae, 0x8101808, 0xba6fd5ba, 0x78f08878, 0x254a6f25, 0x2e5c722e, 0x1c38241c, 0xa657f1a6, 0xb473c7b4, 0xc69751c6, 0xe8cb23e8, 0xdda17cdd, 0x74e89c74, 0x1f3e211f, 0x4b96dd4b, 0xbd61dcbd, 0x8b0d868b, 0x8a0f858a, 0x70e09070, 0x3e7c423e, 0xb571c4b5, 0x66ccaa66, 0x4890d848, 0x3060503, 0xf6f701f6, 0xe1c120e, 0x61c2a361, 0x356a5f35, 0x57aef957, 0xb969d0b9, 0x86179186, 0xc19958c1, 0x1d3a271d, 0x9e27b99e, 0xe1d938e1, 0xf8eb13f8, 0x982bb398, 0x11223311, 0x69d2bb69, 0xd9a970d9, 0x8e07898e, 0x9433a794, 0x9b2db69b, 0x1e3c221e, 0x87159287, 0xe9c920e9, 0xce8749ce, 0x55aaff55, 0x28507828, 0xdfa57adf, 0x8c038f8c, 0xa159f8a1, 0x89098089, 0xd1a170d, 0xbf65dabf, 0xe6d731e6, 0x4284c642, 0x68d0b868, 0x4182c341, 0x9929b099, 0x2d5a772d, 0xf1e110f, 0xb07bcbb0, 0x54a8fc54, 0xbb6dd6bb, 0x162c3a16]
|
||||
fileprivate let T2_INV: Array<UInt32> = [0xf45150a7, 0x417e5365, 0x171ac3a4, 0x273a965e, 0xab3bcb6b, 0x9d1ff145, 0xfaacab58, 0xe34b9303, 0x302055fa, 0x76adf66d, 0xcc889176, 0x2f5254c, 0xe54ffcd7, 0x2ac5d7cb, 0x35268044, 0x62b58fa3, 0xb1de495a, 0xba25671b, 0xea45980e, 0xfe5de1c0, 0x2fc30275, 0x4c8112f0, 0x468da397, 0xd36bc6f9, 0x8f03e75f, 0x9215959c, 0x6dbfeb7a, 0x5295da59, 0xbed42d83, 0x7458d321, 0xe0492969, 0xc98e44c8, 0xc2756a89, 0x8ef47879, 0x58996b3e, 0xb927dd71, 0xe1beb64f, 0x88f017ad, 0x20c966ac, 0xce7db43a, 0xdf63184a, 0x1ae58231, 0x51976033, 0x5362457f, 0x64b1e077, 0x6bbb84ae, 0x81fe1ca0, 0x8f9942b, 0x48705868, 0x458f19fd, 0xde94876c, 0x7b52b7f8, 0x73ab23d3, 0x4b72e202, 0x1fe3578f, 0x55662aab, 0xebb20728, 0xb52f03c2, 0xc5869a7b, 0x37d3a508, 0x2830f287, 0xbf23b2a5, 0x302ba6a, 0x16ed5c82, 0xcf8a2b1c, 0x79a792b4, 0x7f3f0f2, 0x694ea1e2, 0xda65cdf4, 0x506d5be, 0x34d11f62, 0xa6c48afe, 0x2e349d53, 0xf3a2a055, 0x8a0532e1, 0xf6a475eb, 0x830b39ec, 0x6040aaef, 0x715e069f, 0x6ebd5110, 0x213ef98a, 0xdd963d06, 0x3eddae05, 0xe64d46bd, 0x5491b58d, 0xc471055d, 0x6046fd4, 0x5060ff15, 0x981924fb, 0xbdd697e9, 0x4089cc43, 0xd967779e, 0xe8b0bd42, 0x8907888b, 0x19e7385b, 0xc879dbee, 0x7ca1470a, 0x427ce90f, 0x84f8c91e, 0x0, 0x80098386, 0x2b3248ed, 0x111eac70, 0x5a6c4e72, 0xefdfbff, 0x850f5638, 0xae3d1ed5, 0x2d362739, 0xf0a64d9, 0x5c6821a6, 0x5b9bd154, 0x36243a2e, 0xa0cb167, 0x57930fe7, 0xeeb4d296, 0x9b1b9e91, 0xc0804fc5, 0xdc61a220, 0x775a694b, 0x121c161a, 0x93e20aba, 0xa0c0e52a, 0x223c43e0, 0x1b121d17, 0x90e0b0d, 0x8bf2adc7, 0xb62db9a8, 0x1e14c8a9, 0xf1578519, 0x75af4c07, 0x99eebbdd, 0x7fa3fd60, 0x1f79f26, 0x725cbcf5, 0x6644c53b, 0xfb5b347e, 0x438b7629, 0x23cbdcc6, 0xedb668fc, 0xe4b863f1, 0x31d7cadc, 0x63421085, 0x97134022, 0xc6842011, 0x4a857d24, 0xbbd2f83d, 0xf9ae1132, 0x29c76da1, 0x9e1d4b2f, 0xb2dcf330, 0x860dec52, 0xc177d0e3, 0xb32b6c16, 0x70a999b9, 0x9411fa48, 0xe9472264, 0xfca8c48c, 0xf0a01a3f, 0x7d56d82c, 0x3322ef90, 0x4987c74e, 0x38d9c1d1, 0xca8cfea2, 0xd498360b, 0xf5a6cf81, 0x7aa528de, 0xb7da268e, 0xad3fa4bf, 0x3a2ce49d, 0x78500d92, 0x5f6a9bcc, 0x7e546246, 0x8df6c213, 0xd890e8b8, 0x392e5ef7, 0xc382f5af, 0x5d9fbe80, 0xd0697c93, 0xd56fa92d, 0x25cfb312, 0xacc83b99, 0x1810a77d, 0x9ce86e63, 0x3bdb7bbb, 0x26cd0978, 0x596ef418, 0x9aec01b7, 0x4f83a89a, 0x95e6656e, 0xffaa7ee6, 0xbc2108cf, 0x15efe6e8, 0xe7bad99b, 0x6f4ace36, 0x9fead409, 0xb029d67c, 0xa431afb2, 0x3f2a3123, 0xa5c63094, 0xa235c066, 0x4e7437bc, 0x82fca6ca, 0x90e0b0d0, 0xa73315d8, 0x4f14a98, 0xec41f7da, 0xcd7f0e50, 0x91172ff6, 0x4d768dd6, 0xef434db0, 0xaacc544d, 0x96e4df04, 0xd19ee3b5, 0x6a4c1b88, 0x2cc1b81f, 0x65467f51, 0x5e9d04ea, 0x8c015d35, 0x87fa7374, 0xbfb2e41, 0x67b35a1d, 0xdb9252d2, 0x10e93356, 0xd66d1347, 0xd79a8c61, 0xa1377a0c, 0xf8598e14, 0x13eb893c, 0xa9ceee27, 0x61b735c9, 0x1ce1ede5, 0x477a3cb1, 0xd29c59df, 0xf2553f73, 0x141879ce, 0xc773bf37, 0xf753eacd, 0xfd5f5baa, 0x3ddf146f, 0x447886db, 0xafca81f3, 0x68b93ec4, 0x24382c34, 0xa3c25f40, 0x1d1672c3, 0xe2bc0c25, 0x3c288b49, 0xdff4195, 0xa8397101, 0xc08deb3, 0xb4d89ce4, 0x566490c1, 0xcb7b6184, 0x32d570b6, 0x6c48745c, 0xb8d04257]
|
||||
fileprivate let T3: Array<UInt32> = [0xc6a56363, 0xf8847c7c, 0xee997777, 0xf68d7b7b, 0xff0df2f2, 0xd6bd6b6b, 0xdeb16f6f, 0x9154c5c5, 0x60503030, 0x2030101, 0xcea96767, 0x567d2b2b, 0xe719fefe, 0xb562d7d7, 0x4de6abab, 0xec9a7676, 0x8f45caca, 0x1f9d8282, 0x8940c9c9, 0xfa877d7d, 0xef15fafa, 0xb2eb5959, 0x8ec94747, 0xfb0bf0f0, 0x41ecadad, 0xb367d4d4, 0x5ffda2a2, 0x45eaafaf, 0x23bf9c9c, 0x53f7a4a4, 0xe4967272, 0x9b5bc0c0, 0x75c2b7b7, 0xe11cfdfd, 0x3dae9393, 0x4c6a2626, 0x6c5a3636, 0x7e413f3f, 0xf502f7f7, 0x834fcccc, 0x685c3434, 0x51f4a5a5, 0xd134e5e5, 0xf908f1f1, 0xe2937171, 0xab73d8d8, 0x62533131, 0x2a3f1515, 0x80c0404, 0x9552c7c7, 0x46652323, 0x9d5ec3c3, 0x30281818, 0x37a19696, 0xa0f0505, 0x2fb59a9a, 0xe090707, 0x24361212, 0x1b9b8080, 0xdf3de2e2, 0xcd26ebeb, 0x4e692727, 0x7fcdb2b2, 0xea9f7575, 0x121b0909, 0x1d9e8383, 0x58742c2c, 0x342e1a1a, 0x362d1b1b, 0xdcb26e6e, 0xb4ee5a5a, 0x5bfba0a0, 0xa4f65252, 0x764d3b3b, 0xb761d6d6, 0x7dceb3b3, 0x527b2929, 0xdd3ee3e3, 0x5e712f2f, 0x13978484, 0xa6f55353, 0xb968d1d1, 0x0, 0xc12ceded, 0x40602020, 0xe31ffcfc, 0x79c8b1b1, 0xb6ed5b5b, 0xd4be6a6a, 0x8d46cbcb, 0x67d9bebe, 0x724b3939, 0x94de4a4a, 0x98d44c4c, 0xb0e85858, 0x854acfcf, 0xbb6bd0d0, 0xc52aefef, 0x4fe5aaaa, 0xed16fbfb, 0x86c54343, 0x9ad74d4d, 0x66553333, 0x11948585, 0x8acf4545, 0xe910f9f9, 0x4060202, 0xfe817f7f, 0xa0f05050, 0x78443c3c, 0x25ba9f9f, 0x4be3a8a8, 0xa2f35151, 0x5dfea3a3, 0x80c04040, 0x58a8f8f, 0x3fad9292, 0x21bc9d9d, 0x70483838, 0xf104f5f5, 0x63dfbcbc, 0x77c1b6b6, 0xaf75dada, 0x42632121, 0x20301010, 0xe51affff, 0xfd0ef3f3, 0xbf6dd2d2, 0x814ccdcd, 0x18140c0c, 0x26351313, 0xc32fecec, 0xbee15f5f, 0x35a29797, 0x88cc4444, 0x2e391717, 0x9357c4c4, 0x55f2a7a7, 0xfc827e7e, 0x7a473d3d, 0xc8ac6464, 0xbae75d5d, 0x322b1919, 0xe6957373, 0xc0a06060, 0x19988181, 0x9ed14f4f, 0xa37fdcdc, 0x44662222, 0x547e2a2a, 0x3bab9090, 0xb838888, 0x8cca4646, 0xc729eeee, 0x6bd3b8b8, 0x283c1414, 0xa779dede, 0xbce25e5e, 0x161d0b0b, 0xad76dbdb, 0xdb3be0e0, 0x64563232, 0x744e3a3a, 0x141e0a0a, 0x92db4949, 0xc0a0606, 0x486c2424, 0xb8e45c5c, 0x9f5dc2c2, 0xbd6ed3d3, 0x43efacac, 0xc4a66262, 0x39a89191, 0x31a49595, 0xd337e4e4, 0xf28b7979, 0xd532e7e7, 0x8b43c8c8, 0x6e593737, 0xdab76d6d, 0x18c8d8d, 0xb164d5d5, 0x9cd24e4e, 0x49e0a9a9, 0xd8b46c6c, 0xacfa5656, 0xf307f4f4, 0xcf25eaea, 0xcaaf6565, 0xf48e7a7a, 0x47e9aeae, 0x10180808, 0x6fd5baba, 0xf0887878, 0x4a6f2525, 0x5c722e2e, 0x38241c1c, 0x57f1a6a6, 0x73c7b4b4, 0x9751c6c6, 0xcb23e8e8, 0xa17cdddd, 0xe89c7474, 0x3e211f1f, 0x96dd4b4b, 0x61dcbdbd, 0xd868b8b, 0xf858a8a, 0xe0907070, 0x7c423e3e, 0x71c4b5b5, 0xccaa6666, 0x90d84848, 0x6050303, 0xf701f6f6, 0x1c120e0e, 0xc2a36161, 0x6a5f3535, 0xaef95757, 0x69d0b9b9, 0x17918686, 0x9958c1c1, 0x3a271d1d, 0x27b99e9e, 0xd938e1e1, 0xeb13f8f8, 0x2bb39898, 0x22331111, 0xd2bb6969, 0xa970d9d9, 0x7898e8e, 0x33a79494, 0x2db69b9b, 0x3c221e1e, 0x15928787, 0xc920e9e9, 0x8749cece, 0xaaff5555, 0x50782828, 0xa57adfdf, 0x38f8c8c, 0x59f8a1a1, 0x9808989, 0x1a170d0d, 0x65dabfbf, 0xd731e6e6, 0x84c64242, 0xd0b86868, 0x82c34141, 0x29b09999, 0x5a772d2d, 0x1e110f0f, 0x7bcbb0b0, 0xa8fc5454, 0x6dd6bbbb, 0x2c3a1616]
|
||||
fileprivate let T3_INV: Array<UInt32> = [0x5150a7f4, 0x7e536541, 0x1ac3a417, 0x3a965e27, 0x3bcb6bab, 0x1ff1459d, 0xacab58fa, 0x4b9303e3, 0x2055fa30, 0xadf66d76, 0x889176cc, 0xf5254c02, 0x4ffcd7e5, 0xc5d7cb2a, 0x26804435, 0xb58fa362, 0xde495ab1, 0x25671bba, 0x45980eea, 0x5de1c0fe, 0xc302752f, 0x8112f04c, 0x8da39746, 0x6bc6f9d3, 0x3e75f8f, 0x15959c92, 0xbfeb7a6d, 0x95da5952, 0xd42d83be, 0x58d32174, 0x492969e0, 0x8e44c8c9, 0x756a89c2, 0xf478798e, 0x996b3e58, 0x27dd71b9, 0xbeb64fe1, 0xf017ad88, 0xc966ac20, 0x7db43ace, 0x63184adf, 0xe582311a, 0x97603351, 0x62457f53, 0xb1e07764, 0xbb84ae6b, 0xfe1ca081, 0xf9942b08, 0x70586848, 0x8f19fd45, 0x94876cde, 0x52b7f87b, 0xab23d373, 0x72e2024b, 0xe3578f1f, 0x662aab55, 0xb20728eb, 0x2f03c2b5, 0x869a7bc5, 0xd3a50837, 0x30f28728, 0x23b2a5bf, 0x2ba6a03, 0xed5c8216, 0x8a2b1ccf, 0xa792b479, 0xf3f0f207, 0x4ea1e269, 0x65cdf4da, 0x6d5be05, 0xd11f6234, 0xc48afea6, 0x349d532e, 0xa2a055f3, 0x532e18a, 0xa475ebf6, 0xb39ec83, 0x40aaef60, 0x5e069f71, 0xbd51106e, 0x3ef98a21, 0x963d06dd, 0xddae053e, 0x4d46bde6, 0x91b58d54, 0x71055dc4, 0x46fd406, 0x60ff1550, 0x1924fb98, 0xd697e9bd, 0x89cc4340, 0x67779ed9, 0xb0bd42e8, 0x7888b89, 0xe7385b19, 0x79dbeec8, 0xa1470a7c, 0x7ce90f42, 0xf8c91e84, 0x0, 0x9838680, 0x3248ed2b, 0x1eac7011, 0x6c4e725a, 0xfdfbff0e, 0xf563885, 0x3d1ed5ae, 0x3627392d, 0xa64d90f, 0x6821a65c, 0x9bd1545b, 0x243a2e36, 0xcb1670a, 0x930fe757, 0xb4d296ee, 0x1b9e919b, 0x804fc5c0, 0x61a220dc, 0x5a694b77, 0x1c161a12, 0xe20aba93, 0xc0e52aa0, 0x3c43e022, 0x121d171b, 0xe0b0d09, 0xf2adc78b, 0x2db9a8b6, 0x14c8a91e, 0x578519f1, 0xaf4c0775, 0xeebbdd99, 0xa3fd607f, 0xf79f2601, 0x5cbcf572, 0x44c53b66, 0x5b347efb, 0x8b762943, 0xcbdcc623, 0xb668fced, 0xb863f1e4, 0xd7cadc31, 0x42108563, 0x13402297, 0x842011c6, 0x857d244a, 0xd2f83dbb, 0xae1132f9, 0xc76da129, 0x1d4b2f9e, 0xdcf330b2, 0xdec5286, 0x77d0e3c1, 0x2b6c16b3, 0xa999b970, 0x11fa4894, 0x472264e9, 0xa8c48cfc, 0xa01a3ff0, 0x56d82c7d, 0x22ef9033, 0x87c74e49, 0xd9c1d138, 0x8cfea2ca, 0x98360bd4, 0xa6cf81f5, 0xa528de7a, 0xda268eb7, 0x3fa4bfad, 0x2ce49d3a, 0x500d9278, 0x6a9bcc5f, 0x5462467e, 0xf6c2138d, 0x90e8b8d8, 0x2e5ef739, 0x82f5afc3, 0x9fbe805d, 0x697c93d0, 0x6fa92dd5, 0xcfb31225, 0xc83b99ac, 0x10a77d18, 0xe86e639c, 0xdb7bbb3b, 0xcd097826, 0x6ef41859, 0xec01b79a, 0x83a89a4f, 0xe6656e95, 0xaa7ee6ff, 0x2108cfbc, 0xefe6e815, 0xbad99be7, 0x4ace366f, 0xead4099f, 0x29d67cb0, 0x31afb2a4, 0x2a31233f, 0xc63094a5, 0x35c066a2, 0x7437bc4e, 0xfca6ca82, 0xe0b0d090, 0x3315d8a7, 0xf14a9804, 0x41f7daec, 0x7f0e50cd, 0x172ff691, 0x768dd64d, 0x434db0ef, 0xcc544daa, 0xe4df0496, 0x9ee3b5d1, 0x4c1b886a, 0xc1b81f2c, 0x467f5165, 0x9d04ea5e, 0x15d358c, 0xfa737487, 0xfb2e410b, 0xb35a1d67, 0x9252d2db, 0xe9335610, 0x6d1347d6, 0x9a8c61d7, 0x377a0ca1, 0x598e14f8, 0xeb893c13, 0xceee27a9, 0xb735c961, 0xe1ede51c, 0x7a3cb147, 0x9c59dfd2, 0x553f73f2, 0x1879ce14, 0x73bf37c7, 0x53eacdf7, 0x5f5baafd, 0xdf146f3d, 0x7886db44, 0xca81f3af, 0xb93ec468, 0x382c3424, 0xc25f40a3, 0x1672c31d, 0xbc0c25e2, 0x288b493c, 0xff41950d, 0x397101a8, 0x8deb30c, 0xd89ce4b4, 0x6490c156, 0x7b6184cb, 0xd570b632, 0x48745c6c, 0xd04257b8]
|
||||
fileprivate var U1: Array<UInt32> = [0x0, 0xb0d090e, 0x161a121c, 0x1d171b12, 0x2c342438, 0x27392d36, 0x3a2e3624, 0x31233f2a, 0x58684870, 0x5365417e, 0x4e725a6c, 0x457f5362, 0x745c6c48, 0x7f516546, 0x62467e54, 0x694b775a, 0xb0d090e0, 0xbbdd99ee, 0xa6ca82fc, 0xadc78bf2, 0x9ce4b4d8, 0x97e9bdd6, 0x8afea6c4, 0x81f3afca, 0xe8b8d890, 0xe3b5d19e, 0xfea2ca8c, 0xf5afc382, 0xc48cfca8, 0xcf81f5a6, 0xd296eeb4, 0xd99be7ba, 0x7bbb3bdb, 0x70b632d5, 0x6da129c7, 0x66ac20c9, 0x578f1fe3, 0x5c8216ed, 0x41950dff, 0x4a9804f1, 0x23d373ab, 0x28de7aa5, 0x35c961b7, 0x3ec468b9, 0xfe75793, 0x4ea5e9d, 0x19fd458f, 0x12f04c81, 0xcb6bab3b, 0xc066a235, 0xdd71b927, 0xd67cb029, 0xe75f8f03, 0xec52860d, 0xf1459d1f, 0xfa489411, 0x9303e34b, 0x980eea45, 0x8519f157, 0x8e14f859, 0xbf37c773, 0xb43ace7d, 0xa92dd56f, 0xa220dc61, 0xf66d76ad, 0xfd607fa3, 0xe07764b1, 0xeb7a6dbf, 0xda595295, 0xd1545b9b, 0xcc434089, 0xc74e4987, 0xae053edd, 0xa50837d3, 0xb81f2cc1, 0xb31225cf, 0x82311ae5, 0x893c13eb, 0x942b08f9, 0x9f2601f7, 0x46bde64d, 0x4db0ef43, 0x50a7f451, 0x5baafd5f, 0x6a89c275, 0x6184cb7b, 0x7c93d069, 0x779ed967, 0x1ed5ae3d, 0x15d8a733, 0x8cfbc21, 0x3c2b52f, 0x32e18a05, 0x39ec830b, 0x24fb9819, 0x2ff69117, 0x8dd64d76, 0x86db4478, 0x9bcc5f6a, 0x90c15664, 0xa1e2694e, 0xaaef6040, 0xb7f87b52, 0xbcf5725c, 0xd5be0506, 0xdeb30c08, 0xc3a4171a, 0xc8a91e14, 0xf98a213e, 0xf2872830, 0xef903322, 0xe49d3a2c, 0x3d06dd96, 0x360bd498, 0x2b1ccf8a, 0x2011c684, 0x1132f9ae, 0x1a3ff0a0, 0x728ebb2, 0xc25e2bc, 0x656e95e6, 0x6e639ce8, 0x737487fa, 0x78798ef4, 0x495ab1de, 0x4257b8d0, 0x5f40a3c2, 0x544daacc, 0xf7daec41, 0xfcd7e54f, 0xe1c0fe5d, 0xeacdf753, 0xdbeec879, 0xd0e3c177, 0xcdf4da65, 0xc6f9d36b, 0xafb2a431, 0xa4bfad3f, 0xb9a8b62d, 0xb2a5bf23, 0x83868009, 0x888b8907, 0x959c9215, 0x9e919b1b, 0x470a7ca1, 0x4c0775af, 0x51106ebd, 0x5a1d67b3, 0x6b3e5899, 0x60335197, 0x7d244a85, 0x7629438b, 0x1f6234d1, 0x146f3ddf, 0x97826cd, 0x2752fc3, 0x335610e9, 0x385b19e7, 0x254c02f5, 0x2e410bfb, 0x8c61d79a, 0x876cde94, 0x9a7bc586, 0x9176cc88, 0xa055f3a2, 0xab58faac, 0xb64fe1be, 0xbd42e8b0, 0xd4099fea, 0xdf0496e4, 0xc2138df6, 0xc91e84f8, 0xf83dbbd2, 0xf330b2dc, 0xee27a9ce, 0xe52aa0c0, 0x3cb1477a, 0x37bc4e74, 0x2aab5566, 0x21a65c68, 0x10856342, 0x1b886a4c, 0x69f715e, 0xd927850, 0x64d90f0a, 0x6fd40604, 0x72c31d16, 0x79ce1418, 0x48ed2b32, 0x43e0223c, 0x5ef7392e, 0x55fa3020, 0x1b79aec, 0xaba93e2, 0x17ad88f0, 0x1ca081fe, 0x2d83bed4, 0x268eb7da, 0x3b99acc8, 0x3094a5c6, 0x59dfd29c, 0x52d2db92, 0x4fc5c080, 0x44c8c98e, 0x75ebf6a4, 0x7ee6ffaa, 0x63f1e4b8, 0x68fcedb6, 0xb1670a0c, 0xba6a0302, 0xa77d1810, 0xac70111e, 0x9d532e34, 0x965e273a, 0x8b493c28, 0x80443526, 0xe90f427c, 0xe2024b72, 0xff155060, 0xf418596e, 0xc53b6644, 0xce366f4a, 0xd3217458, 0xd82c7d56, 0x7a0ca137, 0x7101a839, 0x6c16b32b, 0x671bba25, 0x5638850f, 0x5d358c01, 0x40229713, 0x4b2f9e1d, 0x2264e947, 0x2969e049, 0x347efb5b, 0x3f73f255, 0xe50cd7f, 0x55dc471, 0x184adf63, 0x1347d66d, 0xcadc31d7, 0xc1d138d9, 0xdcc623cb, 0xd7cb2ac5, 0xe6e815ef, 0xede51ce1, 0xf0f207f3, 0xfbff0efd, 0x92b479a7, 0x99b970a9, 0x84ae6bbb, 0x8fa362b5, 0xbe805d9f, 0xb58d5491, 0xa89a4f83, 0xa397468d]
|
||||
fileprivate var U2: Array<UInt32> = [0x0, 0xd090e0b, 0x1a121c16, 0x171b121d, 0x3424382c, 0x392d3627, 0x2e36243a, 0x233f2a31, 0x68487058, 0x65417e53, 0x725a6c4e, 0x7f536245, 0x5c6c4874, 0x5165467f, 0x467e5462, 0x4b775a69, 0xd090e0b0, 0xdd99eebb, 0xca82fca6, 0xc78bf2ad, 0xe4b4d89c, 0xe9bdd697, 0xfea6c48a, 0xf3afca81, 0xb8d890e8, 0xb5d19ee3, 0xa2ca8cfe, 0xafc382f5, 0x8cfca8c4, 0x81f5a6cf, 0x96eeb4d2, 0x9be7bad9, 0xbb3bdb7b, 0xb632d570, 0xa129c76d, 0xac20c966, 0x8f1fe357, 0x8216ed5c, 0x950dff41, 0x9804f14a, 0xd373ab23, 0xde7aa528, 0xc961b735, 0xc468b93e, 0xe757930f, 0xea5e9d04, 0xfd458f19, 0xf04c8112, 0x6bab3bcb, 0x66a235c0, 0x71b927dd, 0x7cb029d6, 0x5f8f03e7, 0x52860dec, 0x459d1ff1, 0x489411fa, 0x3e34b93, 0xeea4598, 0x19f15785, 0x14f8598e, 0x37c773bf, 0x3ace7db4, 0x2dd56fa9, 0x20dc61a2, 0x6d76adf6, 0x607fa3fd, 0x7764b1e0, 0x7a6dbfeb, 0x595295da, 0x545b9bd1, 0x434089cc, 0x4e4987c7, 0x53eddae, 0x837d3a5, 0x1f2cc1b8, 0x1225cfb3, 0x311ae582, 0x3c13eb89, 0x2b08f994, 0x2601f79f, 0xbde64d46, 0xb0ef434d, 0xa7f45150, 0xaafd5f5b, 0x89c2756a, 0x84cb7b61, 0x93d0697c, 0x9ed96777, 0xd5ae3d1e, 0xd8a73315, 0xcfbc2108, 0xc2b52f03, 0xe18a0532, 0xec830b39, 0xfb981924, 0xf691172f, 0xd64d768d, 0xdb447886, 0xcc5f6a9b, 0xc1566490, 0xe2694ea1, 0xef6040aa, 0xf87b52b7, 0xf5725cbc, 0xbe0506d5, 0xb30c08de, 0xa4171ac3, 0xa91e14c8, 0x8a213ef9, 0x872830f2, 0x903322ef, 0x9d3a2ce4, 0x6dd963d, 0xbd49836, 0x1ccf8a2b, 0x11c68420, 0x32f9ae11, 0x3ff0a01a, 0x28ebb207, 0x25e2bc0c, 0x6e95e665, 0x639ce86e, 0x7487fa73, 0x798ef478, 0x5ab1de49, 0x57b8d042, 0x40a3c25f, 0x4daacc54, 0xdaec41f7, 0xd7e54ffc, 0xc0fe5de1, 0xcdf753ea, 0xeec879db, 0xe3c177d0, 0xf4da65cd, 0xf9d36bc6, 0xb2a431af, 0xbfad3fa4, 0xa8b62db9, 0xa5bf23b2, 0x86800983, 0x8b890788, 0x9c921595, 0x919b1b9e, 0xa7ca147, 0x775af4c, 0x106ebd51, 0x1d67b35a, 0x3e58996b, 0x33519760, 0x244a857d, 0x29438b76, 0x6234d11f, 0x6f3ddf14, 0x7826cd09, 0x752fc302, 0x5610e933, 0x5b19e738, 0x4c02f525, 0x410bfb2e, 0x61d79a8c, 0x6cde9487, 0x7bc5869a, 0x76cc8891, 0x55f3a2a0, 0x58faacab, 0x4fe1beb6, 0x42e8b0bd, 0x99fead4, 0x496e4df, 0x138df6c2, 0x1e84f8c9, 0x3dbbd2f8, 0x30b2dcf3, 0x27a9ceee, 0x2aa0c0e5, 0xb1477a3c, 0xbc4e7437, 0xab55662a, 0xa65c6821, 0x85634210, 0x886a4c1b, 0x9f715e06, 0x9278500d, 0xd90f0a64, 0xd406046f, 0xc31d1672, 0xce141879, 0xed2b3248, 0xe0223c43, 0xf7392e5e, 0xfa302055, 0xb79aec01, 0xba93e20a, 0xad88f017, 0xa081fe1c, 0x83bed42d, 0x8eb7da26, 0x99acc83b, 0x94a5c630, 0xdfd29c59, 0xd2db9252, 0xc5c0804f, 0xc8c98e44, 0xebf6a475, 0xe6ffaa7e, 0xf1e4b863, 0xfcedb668, 0x670a0cb1, 0x6a0302ba, 0x7d1810a7, 0x70111eac, 0x532e349d, 0x5e273a96, 0x493c288b, 0x44352680, 0xf427ce9, 0x24b72e2, 0x155060ff, 0x18596ef4, 0x3b6644c5, 0x366f4ace, 0x217458d3, 0x2c7d56d8, 0xca1377a, 0x1a83971, 0x16b32b6c, 0x1bba2567, 0x38850f56, 0x358c015d, 0x22971340, 0x2f9e1d4b, 0x64e94722, 0x69e04929, 0x7efb5b34, 0x73f2553f, 0x50cd7f0e, 0x5dc47105, 0x4adf6318, 0x47d66d13, 0xdc31d7ca, 0xd138d9c1, 0xc623cbdc, 0xcb2ac5d7, 0xe815efe6, 0xe51ce1ed, 0xf207f3f0, 0xff0efdfb, 0xb479a792, 0xb970a999, 0xae6bbb84, 0xa362b58f, 0x805d9fbe, 0x8d5491b5, 0x9a4f83a8, 0x97468da3]
|
||||
fileprivate var U3: Array<UInt32> = [0x0, 0x90e0b0d, 0x121c161a, 0x1b121d17, 0x24382c34, 0x2d362739, 0x36243a2e, 0x3f2a3123, 0x48705868, 0x417e5365, 0x5a6c4e72, 0x5362457f, 0x6c48745c, 0x65467f51, 0x7e546246, 0x775a694b, 0x90e0b0d0, 0x99eebbdd, 0x82fca6ca, 0x8bf2adc7, 0xb4d89ce4, 0xbdd697e9, 0xa6c48afe, 0xafca81f3, 0xd890e8b8, 0xd19ee3b5, 0xca8cfea2, 0xc382f5af, 0xfca8c48c, 0xf5a6cf81, 0xeeb4d296, 0xe7bad99b, 0x3bdb7bbb, 0x32d570b6, 0x29c76da1, 0x20c966ac, 0x1fe3578f, 0x16ed5c82, 0xdff4195, 0x4f14a98, 0x73ab23d3, 0x7aa528de, 0x61b735c9, 0x68b93ec4, 0x57930fe7, 0x5e9d04ea, 0x458f19fd, 0x4c8112f0, 0xab3bcb6b, 0xa235c066, 0xb927dd71, 0xb029d67c, 0x8f03e75f, 0x860dec52, 0x9d1ff145, 0x9411fa48, 0xe34b9303, 0xea45980e, 0xf1578519, 0xf8598e14, 0xc773bf37, 0xce7db43a, 0xd56fa92d, 0xdc61a220, 0x76adf66d, 0x7fa3fd60, 0x64b1e077, 0x6dbfeb7a, 0x5295da59, 0x5b9bd154, 0x4089cc43, 0x4987c74e, 0x3eddae05, 0x37d3a508, 0x2cc1b81f, 0x25cfb312, 0x1ae58231, 0x13eb893c, 0x8f9942b, 0x1f79f26, 0xe64d46bd, 0xef434db0, 0xf45150a7, 0xfd5f5baa, 0xc2756a89, 0xcb7b6184, 0xd0697c93, 0xd967779e, 0xae3d1ed5, 0xa73315d8, 0xbc2108cf, 0xb52f03c2, 0x8a0532e1, 0x830b39ec, 0x981924fb, 0x91172ff6, 0x4d768dd6, 0x447886db, 0x5f6a9bcc, 0x566490c1, 0x694ea1e2, 0x6040aaef, 0x7b52b7f8, 0x725cbcf5, 0x506d5be, 0xc08deb3, 0x171ac3a4, 0x1e14c8a9, 0x213ef98a, 0x2830f287, 0x3322ef90, 0x3a2ce49d, 0xdd963d06, 0xd498360b, 0xcf8a2b1c, 0xc6842011, 0xf9ae1132, 0xf0a01a3f, 0xebb20728, 0xe2bc0c25, 0x95e6656e, 0x9ce86e63, 0x87fa7374, 0x8ef47879, 0xb1de495a, 0xb8d04257, 0xa3c25f40, 0xaacc544d, 0xec41f7da, 0xe54ffcd7, 0xfe5de1c0, 0xf753eacd, 0xc879dbee, 0xc177d0e3, 0xda65cdf4, 0xd36bc6f9, 0xa431afb2, 0xad3fa4bf, 0xb62db9a8, 0xbf23b2a5, 0x80098386, 0x8907888b, 0x9215959c, 0x9b1b9e91, 0x7ca1470a, 0x75af4c07, 0x6ebd5110, 0x67b35a1d, 0x58996b3e, 0x51976033, 0x4a857d24, 0x438b7629, 0x34d11f62, 0x3ddf146f, 0x26cd0978, 0x2fc30275, 0x10e93356, 0x19e7385b, 0x2f5254c, 0xbfb2e41, 0xd79a8c61, 0xde94876c, 0xc5869a7b, 0xcc889176, 0xf3a2a055, 0xfaacab58, 0xe1beb64f, 0xe8b0bd42, 0x9fead409, 0x96e4df04, 0x8df6c213, 0x84f8c91e, 0xbbd2f83d, 0xb2dcf330, 0xa9ceee27, 0xa0c0e52a, 0x477a3cb1, 0x4e7437bc, 0x55662aab, 0x5c6821a6, 0x63421085, 0x6a4c1b88, 0x715e069f, 0x78500d92, 0xf0a64d9, 0x6046fd4, 0x1d1672c3, 0x141879ce, 0x2b3248ed, 0x223c43e0, 0x392e5ef7, 0x302055fa, 0x9aec01b7, 0x93e20aba, 0x88f017ad, 0x81fe1ca0, 0xbed42d83, 0xb7da268e, 0xacc83b99, 0xa5c63094, 0xd29c59df, 0xdb9252d2, 0xc0804fc5, 0xc98e44c8, 0xf6a475eb, 0xffaa7ee6, 0xe4b863f1, 0xedb668fc, 0xa0cb167, 0x302ba6a, 0x1810a77d, 0x111eac70, 0x2e349d53, 0x273a965e, 0x3c288b49, 0x35268044, 0x427ce90f, 0x4b72e202, 0x5060ff15, 0x596ef418, 0x6644c53b, 0x6f4ace36, 0x7458d321, 0x7d56d82c, 0xa1377a0c, 0xa8397101, 0xb32b6c16, 0xba25671b, 0x850f5638, 0x8c015d35, 0x97134022, 0x9e1d4b2f, 0xe9472264, 0xe0492969, 0xfb5b347e, 0xf2553f73, 0xcd7f0e50, 0xc471055d, 0xdf63184a, 0xd66d1347, 0x31d7cadc, 0x38d9c1d1, 0x23cbdcc6, 0x2ac5d7cb, 0x15efe6e8, 0x1ce1ede5, 0x7f3f0f2, 0xefdfbff, 0x79a792b4, 0x70a999b9, 0x6bbb84ae, 0x62b58fa3, 0x5d9fbe80, 0x5491b58d, 0x4f83a89a, 0x468da397]
|
||||
fileprivate var U4: Array<UInt32> = [0x0, 0xe0b0d09, 0x1c161a12, 0x121d171b, 0x382c3424, 0x3627392d, 0x243a2e36, 0x2a31233f, 0x70586848, 0x7e536541, 0x6c4e725a, 0x62457f53, 0x48745c6c, 0x467f5165, 0x5462467e, 0x5a694b77, 0xe0b0d090, 0xeebbdd99, 0xfca6ca82, 0xf2adc78b, 0xd89ce4b4, 0xd697e9bd, 0xc48afea6, 0xca81f3af, 0x90e8b8d8, 0x9ee3b5d1, 0x8cfea2ca, 0x82f5afc3, 0xa8c48cfc, 0xa6cf81f5, 0xb4d296ee, 0xbad99be7, 0xdb7bbb3b, 0xd570b632, 0xc76da129, 0xc966ac20, 0xe3578f1f, 0xed5c8216, 0xff41950d, 0xf14a9804, 0xab23d373, 0xa528de7a, 0xb735c961, 0xb93ec468, 0x930fe757, 0x9d04ea5e, 0x8f19fd45, 0x8112f04c, 0x3bcb6bab, 0x35c066a2, 0x27dd71b9, 0x29d67cb0, 0x3e75f8f, 0xdec5286, 0x1ff1459d, 0x11fa4894, 0x4b9303e3, 0x45980eea, 0x578519f1, 0x598e14f8, 0x73bf37c7, 0x7db43ace, 0x6fa92dd5, 0x61a220dc, 0xadf66d76, 0xa3fd607f, 0xb1e07764, 0xbfeb7a6d, 0x95da5952, 0x9bd1545b, 0x89cc4340, 0x87c74e49, 0xddae053e, 0xd3a50837, 0xc1b81f2c, 0xcfb31225, 0xe582311a, 0xeb893c13, 0xf9942b08, 0xf79f2601, 0x4d46bde6, 0x434db0ef, 0x5150a7f4, 0x5f5baafd, 0x756a89c2, 0x7b6184cb, 0x697c93d0, 0x67779ed9, 0x3d1ed5ae, 0x3315d8a7, 0x2108cfbc, 0x2f03c2b5, 0x532e18a, 0xb39ec83, 0x1924fb98, 0x172ff691, 0x768dd64d, 0x7886db44, 0x6a9bcc5f, 0x6490c156, 0x4ea1e269, 0x40aaef60, 0x52b7f87b, 0x5cbcf572, 0x6d5be05, 0x8deb30c, 0x1ac3a417, 0x14c8a91e, 0x3ef98a21, 0x30f28728, 0x22ef9033, 0x2ce49d3a, 0x963d06dd, 0x98360bd4, 0x8a2b1ccf, 0x842011c6, 0xae1132f9, 0xa01a3ff0, 0xb20728eb, 0xbc0c25e2, 0xe6656e95, 0xe86e639c, 0xfa737487, 0xf478798e, 0xde495ab1, 0xd04257b8, 0xc25f40a3, 0xcc544daa, 0x41f7daec, 0x4ffcd7e5, 0x5de1c0fe, 0x53eacdf7, 0x79dbeec8, 0x77d0e3c1, 0x65cdf4da, 0x6bc6f9d3, 0x31afb2a4, 0x3fa4bfad, 0x2db9a8b6, 0x23b2a5bf, 0x9838680, 0x7888b89, 0x15959c92, 0x1b9e919b, 0xa1470a7c, 0xaf4c0775, 0xbd51106e, 0xb35a1d67, 0x996b3e58, 0x97603351, 0x857d244a, 0x8b762943, 0xd11f6234, 0xdf146f3d, 0xcd097826, 0xc302752f, 0xe9335610, 0xe7385b19, 0xf5254c02, 0xfb2e410b, 0x9a8c61d7, 0x94876cde, 0x869a7bc5, 0x889176cc, 0xa2a055f3, 0xacab58fa, 0xbeb64fe1, 0xb0bd42e8, 0xead4099f, 0xe4df0496, 0xf6c2138d, 0xf8c91e84, 0xd2f83dbb, 0xdcf330b2, 0xceee27a9, 0xc0e52aa0, 0x7a3cb147, 0x7437bc4e, 0x662aab55, 0x6821a65c, 0x42108563, 0x4c1b886a, 0x5e069f71, 0x500d9278, 0xa64d90f, 0x46fd406, 0x1672c31d, 0x1879ce14, 0x3248ed2b, 0x3c43e022, 0x2e5ef739, 0x2055fa30, 0xec01b79a, 0xe20aba93, 0xf017ad88, 0xfe1ca081, 0xd42d83be, 0xda268eb7, 0xc83b99ac, 0xc63094a5, 0x9c59dfd2, 0x9252d2db, 0x804fc5c0, 0x8e44c8c9, 0xa475ebf6, 0xaa7ee6ff, 0xb863f1e4, 0xb668fced, 0xcb1670a, 0x2ba6a03, 0x10a77d18, 0x1eac7011, 0x349d532e, 0x3a965e27, 0x288b493c, 0x26804435, 0x7ce90f42, 0x72e2024b, 0x60ff1550, 0x6ef41859, 0x44c53b66, 0x4ace366f, 0x58d32174, 0x56d82c7d, 0x377a0ca1, 0x397101a8, 0x2b6c16b3, 0x25671bba, 0xf563885, 0x15d358c, 0x13402297, 0x1d4b2f9e, 0x472264e9, 0x492969e0, 0x5b347efb, 0x553f73f2, 0x7f0e50cd, 0x71055dc4, 0x63184adf, 0x6d1347d6, 0xd7cadc31, 0xd9c1d138, 0xcbdcc623, 0xc5d7cb2a, 0xefe6e815, 0xe1ede51c, 0xf3f0f207, 0xfdfbff0e, 0xa792b479, 0xa999b970, 0xbb84ae6b, 0xb58fa362, 0x9fbe805d, 0x91b58d54, 0x83a89a4f, 0x8da39746]
|
||||
// swiftlint:enable line_length
|
||||
|
||||
init(key: [UInt8], iv: [UInt8]) throws {
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
}
|
||||
|
||||
/**
|
||||
Encrypt message. If padding is necessary, then PKCS7 padding is added and needs to be removed after decryption.
|
||||
|
||||
- parameter message: Plaintext data
|
||||
- parameter padding: Optional padding
|
||||
|
||||
- returns: Encrypted data
|
||||
*/
|
||||
|
||||
func encrypt(bytes: [UInt8]) throws -> [UInt8] {
|
||||
let finalBytes = PKCS7().add(bytes: bytes, blockSize: AESCipher.blockSize)
|
||||
let blocks = finalBytes.chunks(size: AESCipher.blockSize)
|
||||
return try blockMode.encrypt(blocks: blocks, iv: self.iv, cipherOperation: encrypt)
|
||||
}
|
||||
|
||||
private func encrypt(block: [UInt8]) -> [UInt8]? {
|
||||
let rounds = self.variant.Nr
|
||||
let rk = self.expandedKey
|
||||
var b = toUInt32Array(slice: block[block.startIndex..<block.endIndex])
|
||||
|
||||
var t = [UInt32](repeating: 0, count: 4)
|
||||
|
||||
for r in 0..<rounds - 1 {
|
||||
t[0] = b[0] ^ rk[r][0]
|
||||
t[1] = b[1] ^ rk[r][1]
|
||||
t[2] = b[2] ^ rk[r][2]
|
||||
t[3] = b[3] ^ rk[r][3]
|
||||
|
||||
let lb00 = T0[Int(t[0] & 0xFF)]
|
||||
let lb01 = T1[Int((t[1] >> 8) & 0xFF)]
|
||||
let lb02 = T2[Int((t[2] >> 16) & 0xFF)]
|
||||
let lb03 = T3[Int(t[3] >> 24)]
|
||||
b[0] = lb00 ^ lb01 ^ lb02 ^ lb03
|
||||
|
||||
let lb10 = T0[Int(t[1] & 0xFF)]
|
||||
let lb11 = T1[Int((t[2] >> 8) & 0xFF)]
|
||||
let lb12 = T2[Int((t[3] >> 16) & 0xFF)]
|
||||
let lb13 = T3[Int(t[0] >> 24)]
|
||||
b[1] = lb10 ^ lb11 ^ lb12 ^ lb13
|
||||
|
||||
let lb20 = T0[Int(t[2] & 0xFF)]
|
||||
let lb21 = T1[Int((t[3] >> 8) & 0xFF)]
|
||||
let lb22 = T2[Int((t[0] >> 16) & 0xFF)]
|
||||
let lb23 = T3[Int(t[1] >> 24)]
|
||||
b[2] = lb20 ^ lb21 ^ lb22 ^ lb23
|
||||
|
||||
let lb30 = T0[Int(t[3] & 0xFF)]
|
||||
let lb31 = T1[Int((t[0] >> 8) & 0xFF)]
|
||||
let lb32 = T2[Int((t[1] >> 16) & 0xFF)]
|
||||
let lb33 = T3[Int(t[2] >> 24)]
|
||||
b[3] = lb30 ^ lb31 ^ lb32 ^ lb33
|
||||
}
|
||||
|
||||
// last round
|
||||
let r = rounds - 1
|
||||
|
||||
t[0] = b[0] ^ rk[r][0]
|
||||
t[1] = b[1] ^ rk[r][1]
|
||||
t[2] = b[2] ^ rk[r][2]
|
||||
t[3] = b[3] ^ rk[r][3]
|
||||
|
||||
// rounds
|
||||
b[0] = F1(t[0], t[1], t[2], t[3]) ^ rk[rounds][0]
|
||||
b[1] = F1(t[1], t[2], t[3], t[0]) ^ rk[rounds][1]
|
||||
b[2] = F1(t[2], t[3], t[0], t[1]) ^ rk[rounds][2]
|
||||
b[3] = F1(t[3], t[0], t[1], t[2]) ^ rk[rounds][3]
|
||||
|
||||
var out = [UInt8]()
|
||||
out.reserveCapacity(b.count * 4)
|
||||
for num in b {
|
||||
out.append(UInt8(num & 0xFF))
|
||||
out.append(UInt8((num >> 8) & 0xFF))
|
||||
out.append(UInt8((num >> 16) & 0xFF))
|
||||
out.append(UInt8((num >> 24) & 0xFF))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func decrypt(bytes: [UInt8]) throws -> [UInt8] {
|
||||
if (bytes.count % AESCipher.blockSize) != 0 {
|
||||
throw Error.blockSizeExceeded
|
||||
}
|
||||
|
||||
let blocks = bytes.chunks(size: AESCipher.blockSize)
|
||||
return try PKCS7().remove(bytes: blockMode.decrypt(blocks: blocks, iv: self.iv, cipherOperation: decrypt), blockSize: AESCipher.blockSize)
|
||||
}
|
||||
|
||||
private func decrypt(block: [UInt8]) -> [UInt8]? {
|
||||
let rounds = self.variant.Nr
|
||||
let rk = expandedKeyInv
|
||||
var b = toUInt32Array(slice: block[block.startIndex..<block.endIndex])
|
||||
|
||||
var t = [UInt32](repeating: 0, count: 4)
|
||||
|
||||
for r in (2...rounds).reversed() {
|
||||
t[0] = b[0] ^ rk[r][0]
|
||||
t[1] = b[1] ^ rk[r][1]
|
||||
t[2] = b[2] ^ rk[r][2]
|
||||
t[3] = b[3] ^ rk[r][3]
|
||||
|
||||
let b00 = T0_INV[Int(t[0] & 0xFF)]
|
||||
let b01 = T1_INV[Int((t[3] >> 8) & 0xFF)]
|
||||
let b02 = T2_INV[Int((t[2] >> 16) & 0xFF)]
|
||||
let b03 = T3_INV[Int(t[1] >> 24)]
|
||||
b[0] = b00 ^ b01 ^ b02 ^ b03
|
||||
|
||||
let b10 = T0_INV[Int(t[1] & 0xFF)]
|
||||
let b11 = T1_INV[Int((t[0] >> 8) & 0xFF)]
|
||||
let b12 = T2_INV[Int((t[3] >> 16) & 0xFF)]
|
||||
let b13 = T3_INV[Int(t[2] >> 24)]
|
||||
b[1] = b10 ^ b11 ^ b12 ^ b13
|
||||
|
||||
let b20 = T0_INV[Int(t[2] & 0xFF)]
|
||||
let b21 = T1_INV[Int((t[1] >> 8) & 0xFF)]
|
||||
let b22 = T2_INV[Int((t[0] >> 16) & 0xFF)]
|
||||
let b23 = T3_INV[Int(t[3] >> 24)]
|
||||
b[2] = b20 ^ b21 ^ b22 ^ b23
|
||||
|
||||
let b30 = T0_INV[Int(t[3] & 0xFF)]
|
||||
let b31 = T1_INV[Int((t[2] >> 8) & 0xFF)]
|
||||
let b32 = T2_INV[Int((t[1] >> 16) & 0xFF)]
|
||||
let b33 = T3_INV[Int(t[0] >> 24)]
|
||||
b[3] = b30 ^ b31 ^ b32 ^ b33
|
||||
}
|
||||
|
||||
// last round
|
||||
t[0] = b[0] ^ rk[1][0]
|
||||
t[1] = b[1] ^ rk[1][1]
|
||||
t[2] = b[2] ^ rk[1][2]
|
||||
t[3] = b[3] ^ rk[1][3]
|
||||
|
||||
// rounds
|
||||
|
||||
let lb00 = sBoxInv[Int(B0(t[0]))]
|
||||
let lb01 = (sBoxInv[Int(B1(t[3]))] << 8)
|
||||
let lb02 = (sBoxInv[Int(B2(t[2]))] << 16)
|
||||
let lb03 = (sBoxInv[Int(B3(t[1]))] << 24)
|
||||
b[0] = lb00 | lb01 | lb02 | lb03 ^ rk[0][0]
|
||||
|
||||
let lb10 = sBoxInv[Int(B0(t[1]))]
|
||||
let lb11 = (sBoxInv[Int(B1(t[0]))] << 8)
|
||||
let lb12 = (sBoxInv[Int(B2(t[3]))] << 16)
|
||||
let lb13 = (sBoxInv[Int(B3(t[2]))] << 24)
|
||||
b[1] = lb10 | lb11 | lb12 | lb13 ^ rk[0][1]
|
||||
|
||||
let lb20 = sBoxInv[Int(B0(t[2]))]
|
||||
let lb21 = (sBoxInv[Int(B1(t[1]))] << 8)
|
||||
let lb22 = (sBoxInv[Int(B2(t[0]))] << 16)
|
||||
let lb23 = (sBoxInv[Int(B3(t[3]))] << 24)
|
||||
b[2] = lb20 | lb21 | lb22 | lb23 ^ rk[0][2]
|
||||
|
||||
let lb30 = sBoxInv[Int(B0(t[3]))]
|
||||
let lb31 = (sBoxInv[Int(B1(t[2]))] << 8)
|
||||
let lb32 = (sBoxInv[Int(B2(t[1]))] << 16)
|
||||
let lb33 = (sBoxInv[Int(B3(t[0]))] << 24)
|
||||
b[3] = lb30 | lb31 | lb32 | lb33 ^ rk[0][3]
|
||||
|
||||
var out = [UInt8]()
|
||||
out.reserveCapacity(b.count * 4)
|
||||
for num in b {
|
||||
out.append(UInt8(num & 0xFF))
|
||||
out.append(UInt8((num >> 8) & 0xFF))
|
||||
out.append(UInt8((num >> 16) & 0xFF))
|
||||
out.append(UInt8((num >> 24) & 0xFF))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
fileprivate func expandKeyInv(_ key: Key, variant: Variant) -> Array<Array<UInt32>> {
|
||||
let rounds = variant.Nr
|
||||
var rk2: Array<Array<UInt32>> = expandKey(key, variant: variant)
|
||||
|
||||
for r in 1..<rounds {
|
||||
var w: UInt32
|
||||
|
||||
w = rk2[r][0]
|
||||
let u1 = U1[Int(B0(w))] ^ U2[Int(B1(w))]
|
||||
let u2 = U3[Int(B2(w))] ^ U4[Int(B3(w))]
|
||||
rk2[r][0] = u1 ^ u2
|
||||
|
||||
w = rk2[r][1]
|
||||
let u11 = U1[Int(B0(w))] ^ U2[Int(B1(w))]
|
||||
let u12 = U3[Int(B2(w))] ^ U4[Int(B3(w))]
|
||||
rk2[r][1] = u11 ^ u12
|
||||
|
||||
w = rk2[r][2]
|
||||
let u22 = U1[Int(B0(w))] ^ U2[Int(B1(w))]
|
||||
let u23 = U3[Int(B2(w))] ^ U4[Int(B3(w))]
|
||||
rk2[r][2] = u22 ^ u23
|
||||
|
||||
w = rk2[r][3]
|
||||
let u33 = U1[Int(B0(w))] ^ U2[Int(B1(w))]
|
||||
let u34 = U3[Int(B2(w))] ^ U4[Int(B3(w))]
|
||||
rk2[r][3] = u33 ^ u34
|
||||
}
|
||||
|
||||
return rk2
|
||||
}
|
||||
|
||||
fileprivate func expandKey(_ key: Key, variant: Variant) -> Array<Array<UInt32>> {
|
||||
|
||||
func convertExpandedKey(_ expanded: Array<UInt8>) -> Array<Array<UInt32>> {
|
||||
var arr = Array<UInt32>()
|
||||
for idx in stride(from: expanded.startIndex, to: expanded.endIndex, by: 4) {
|
||||
let four = Array(expanded[idx..<idx.advanced(by: 4)].reversed())
|
||||
let num = UInt32(bytes: four)
|
||||
arr.append(num)
|
||||
}
|
||||
|
||||
var allarr = Array<Array<UInt32>>()
|
||||
for idx in stride(from: arr.startIndex, to: arr.endIndex, by: 4) {
|
||||
allarr.append(Array(arr[idx..<idx.advanced(by: 4)]))
|
||||
}
|
||||
return allarr
|
||||
}
|
||||
|
||||
/*
|
||||
* Function used in the Key Expansion routine that takes a four-byte
|
||||
* input word and applies an S-box to each of the four bytes to
|
||||
* produce an output word.
|
||||
*/
|
||||
func subWord(_ word: Array<UInt8>) -> Array<UInt8> {
|
||||
var result = word
|
||||
for i in 0..<4 {
|
||||
result[i] = UInt8(sBox[Int(word[i])])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var w = Array<UInt8>(repeating: 0, count: variant.Nb * (variant.Nr + 1) * 4)
|
||||
for i in 0..<variant.Nk {
|
||||
for wordIdx in 0..<4 {
|
||||
w[(4*i)+wordIdx] = key[(4*i)+wordIdx]
|
||||
}
|
||||
}
|
||||
|
||||
var tmp: Array<UInt8>
|
||||
|
||||
for i in variant.Nk..<variant.Nb * (variant.Nr + 1) {
|
||||
tmp = Array<UInt8>(repeating: 0, count: 4)
|
||||
|
||||
for wordIdx in 0..<4 {
|
||||
tmp[wordIdx] = w[4*(i-1)+wordIdx]
|
||||
}
|
||||
if (i % variant.Nk) == 0 {
|
||||
tmp = subWord(rotateLeft(UInt32(bytes: tmp), by: 8).bytes(totalBytes: MemoryLayout<UInt32>.size))
|
||||
tmp[0] = tmp.first! ^ Rcon[i/variant.Nk]
|
||||
} else if variant.Nk > 6 && (i % variant.Nk) == 4 {
|
||||
tmp = subWord(tmp)
|
||||
}
|
||||
|
||||
// xor array of bytes
|
||||
for wordIdx in 0..<4 {
|
||||
w[4*i+wordIdx] = w[4*(i-variant.Nk)+wordIdx]^tmp[wordIdx]
|
||||
}
|
||||
}
|
||||
return convertExpandedKey(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AESCipher {
|
||||
|
||||
fileprivate func B0(_ x: UInt32) -> UInt32 {
|
||||
return x & 0xFF
|
||||
}
|
||||
|
||||
fileprivate func B1(_ x: UInt32) -> UInt32 {
|
||||
return (x >> 8) & 0xFF
|
||||
}
|
||||
|
||||
fileprivate func B2(_ x: UInt32) -> UInt32 {
|
||||
return (x >> 16) & 0xFF
|
||||
}
|
||||
|
||||
fileprivate func B3(_ x: UInt32) -> UInt32 {
|
||||
return (x >> 24) & 0xFF
|
||||
}
|
||||
|
||||
fileprivate func F1(_ x0: UInt32, _ x1: UInt32, _ x2: UInt32, _ x3: UInt32) -> UInt32 {
|
||||
var result: UInt32 = 0
|
||||
result |= UInt32(B1(T0[Int(x0 & 255)]))
|
||||
result |= UInt32(B1(T0[Int((x1 >> 8) & 255)])) << 8
|
||||
result |= UInt32(B1(T0[Int((x2 >> 16) & 255)])) << 16
|
||||
result |= UInt32(B1(T0[Int(x3 >> 24)])) << 24
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func calculateSBox() -> (sBox: Array<UInt32>, invSBox: Array<UInt32>) {
|
||||
var sbox = Array<UInt32>(repeating: 0, count: 256)
|
||||
var invsbox = sbox
|
||||
sbox[0] = 0x63
|
||||
|
||||
var p: UInt8 = 1, q: UInt8 = 1
|
||||
|
||||
repeat {
|
||||
#if swift(>=4.0)
|
||||
p = p ^ (UInt8(truncatingIfNeeded: Int(p) << 1) ^ ((p & 0x80) == 0x80 ? 0x1B : 0))
|
||||
#else
|
||||
p = p ^ (UInt8(truncatingBitPattern: Int(p) << 1) ^ ((p & 0x80) == 0x80 ? 0x1B : 0))
|
||||
|
||||
#endif
|
||||
q ^= q << 1
|
||||
q ^= q << 2
|
||||
q ^= q << 4
|
||||
q ^= (q & 0x80) == 0x80 ? 0x09 : 0
|
||||
|
||||
let s = 0x63 ^ q ^ rotateLeft(q, by: 1) ^ rotateLeft(q, by: 2) ^ rotateLeft(q, by: 3) ^ rotateLeft(q, by: 4)
|
||||
|
||||
sbox[Int(p)] = UInt32(s)
|
||||
invsbox[Int(s)] = UInt32(p)
|
||||
} while (p != 1)
|
||||
|
||||
return (sBox: sbox, invSBox: invsbox)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Foundation
|
||||
|
||||
extension AESCipher {
|
||||
|
||||
convenience init(key: String, iv: String) throws {
|
||||
guard let kkey = key.data(using: String.Encoding.utf8, allowLossyConversion: false)?.bytes,
|
||||
// swiftlint:disable conditional_binding_cascade
|
||||
let iiv = iv.data(using: String.Encoding.utf8, allowLossyConversion: false)?.bytes else {
|
||||
// swiftlint:disable conditional_binding_cascade
|
||||
throw Error.invalidKeyOrInitializationVector
|
||||
}
|
||||
|
||||
try self.init(key: kkey, iv: iiv)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBC
|
||||
|
||||
// I have no better name for that
|
||||
private typealias CipherOperationOnBlock = ([UInt8]) -> [UInt8]?
|
||||
|
||||
private struct CBCBlockMode {
|
||||
enum BlockError: Swift.Error {
|
||||
case MissingInitializationVector
|
||||
}
|
||||
|
||||
func encrypt(blocks: [[UInt8]], iv: [UInt8]?, cipherOperation: CipherOperationOnBlock) throws -> [UInt8] {
|
||||
precondition(!blocks.isEmpty)
|
||||
guard let iv = iv else {
|
||||
throw BlockError.MissingInitializationVector
|
||||
}
|
||||
|
||||
var out: [UInt8] = [UInt8]()
|
||||
out.reserveCapacity(blocks.count * blocks[blocks.startIndex].count)
|
||||
var prevCiphertext = iv // for the first time prevCiphertext = iv
|
||||
for plaintext in blocks {
|
||||
if let encrypted = cipherOperation(xor(prevCiphertext, plaintext)) {
|
||||
out.append(contentsOf: encrypted)
|
||||
prevCiphertext = encrypted
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decrypt(blocks: [[UInt8]], iv: [UInt8]?, cipherOperation: CipherOperationOnBlock) throws -> [UInt8] {
|
||||
precondition(!blocks.isEmpty)
|
||||
guard let iv = iv else {
|
||||
throw BlockError.MissingInitializationVector
|
||||
}
|
||||
|
||||
var out: [UInt8] = [UInt8]()
|
||||
out.reserveCapacity(blocks.count * blocks[blocks.startIndex].count)
|
||||
var prevCiphertext = iv // for the first time prevCiphertext = iv
|
||||
for ciphertext in blocks {
|
||||
if let decrypted = cipherOperation(ciphertext) { // decrypt
|
||||
out.append(contentsOf: xor(prevCiphertext, decrypted))
|
||||
}
|
||||
prevCiphertext = ciphertext
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Padding
|
||||
|
||||
private struct PKCS7 {
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func add(bytes: [UInt8], blockSize: Int) -> [UInt8] {
|
||||
let padding = UInt8(blockSize - (bytes.count % blockSize))
|
||||
var withPadding = bytes
|
||||
if padding == 0 {
|
||||
// If the original data is a multiple of N bytes, then an extra block of bytes with value N is added.
|
||||
for _ in 0..<blockSize {
|
||||
withPadding.append(contentsOf: [UInt8(blockSize)])
|
||||
}
|
||||
} else {
|
||||
// The value of each added byte is the number of bytes that are added
|
||||
for _ in 0..<padding {
|
||||
withPadding.append(contentsOf: [UInt8(padding)])
|
||||
}
|
||||
}
|
||||
return withPadding
|
||||
}
|
||||
|
||||
func remove(bytes: [UInt8], blockSize: Int?) -> [UInt8] {
|
||||
let lastByte = bytes.last!
|
||||
let padding = Int(lastByte) // last byte
|
||||
let finalLength = bytes.count - padding
|
||||
|
||||
if finalLength < 0 {
|
||||
return bytes
|
||||
}
|
||||
|
||||
if padding >= 1 {
|
||||
return Array(bytes[0..<finalLength])
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
|
||||
private func xor(_ a: Array<UInt8>, _ b: Array<UInt8>) -> Array<UInt8> {
|
||||
var xored = Array<UInt8>(repeating: 0, count: min(a.count, b.count))
|
||||
for i in 0..<xored.count {
|
||||
xored[i] = a[i] ^ b[i]
|
||||
}
|
||||
return xored
|
||||
}
|
||||
|
||||
private func rotateLeft(_ value: UInt8, by: UInt8) -> UInt8 {
|
||||
return ((value << by) & 0xFF) | (value >> (8 - by))
|
||||
}
|
||||
|
||||
private func rotateLeft(_ value: UInt32, by: UInt32) -> UInt32 {
|
||||
return ((value << by) & 0xFFFFFFFF) | (value >> (32 - by))
|
||||
}
|
||||
|
||||
private protocol BitshiftOperationsType {
|
||||
static func << (lhs: Self, rhs: Self) -> Self
|
||||
}
|
||||
|
||||
private protocol ByteConvertible {
|
||||
init(_ value: UInt8)
|
||||
init(truncatingBitPattern: UInt64)
|
||||
}
|
||||
|
||||
#if swift(>=4.0)
|
||||
#else
|
||||
extension UInt32: BitshiftOperationsType, ByteConvertible { }
|
||||
#endif
|
||||
|
||||
fileprivate extension UInt32 {
|
||||
init<T: Collection>(bytes: T) where T.Iterator.Element == UInt8, T.Index == Int {
|
||||
self = bytes.toInteger()
|
||||
}
|
||||
|
||||
func bytes(totalBytes: Int = MemoryLayout<UInt32>.size) -> Array<UInt8> {
|
||||
return arrayOfBytes(value: self, length: totalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
private func toUInt32Array(slice: ArraySlice<UInt8>) -> Array<UInt32> {
|
||||
var result = Array<UInt32>()
|
||||
result.reserveCapacity(16)
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
|
||||
var val: UInt32 = 0
|
||||
val |= slice.count > 3 ? UInt32(slice[idx.advanced(by: 3)]) << 24 : 0
|
||||
val |= slice.count > 2 ? UInt32(slice[idx.advanced(by: 2)]) << 16 : 0
|
||||
val |= slice.count > 1 ? UInt32(slice[idx.advanced(by: 1)]) << 8 : 0
|
||||
val |= !slice.isEmpty ? UInt32(slice[idx]) : 0
|
||||
result.append(val)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Array of bytes, little-endian representation. Don't use if not necessary.
|
||||
/// I found this method slow
|
||||
private func arrayOfBytes<T>(value: T, length: Int? = nil) -> Array<UInt8> {
|
||||
let totalBytes = length ?? MemoryLayout<T>.size
|
||||
|
||||
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
valuePointer.pointee = value
|
||||
|
||||
let bytesPointer = UnsafeMutablePointer<UInt8>(OpaquePointer(valuePointer))
|
||||
var bytes = Array<UInt8>(repeating: 0, count: totalBytes)
|
||||
for j in 0..<min(MemoryLayout<T>.size, totalBytes) {
|
||||
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
|
||||
}
|
||||
|
||||
#if swift(>=4.1)
|
||||
valuePointer.deinitialize(count: 1)
|
||||
valuePointer.deallocate()
|
||||
#else
|
||||
valuePointer.deinitialize()
|
||||
valuePointer.deallocate(capacity: 1)
|
||||
#endif
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
fileprivate extension Collection where Self.Iterator.Element == UInt8, Self.Index == Int {
|
||||
#if swift(>=4.0)
|
||||
func toInteger<T>() -> T where T: FixedWidthInteger {
|
||||
if self.isEmpty {
|
||||
return 0
|
||||
}
|
||||
|
||||
let size = MemoryLayout<T>.size
|
||||
var bytes = self.reversed()
|
||||
if bytes.count < MemoryLayout<T>.size {
|
||||
let paddingCount = MemoryLayout<T>.size - bytes.count
|
||||
if paddingCount > 0 {
|
||||
bytes += Array<UInt8>(repeating: 0, count: paddingCount)
|
||||
}
|
||||
}
|
||||
|
||||
if size == 1 {
|
||||
return T(truncatingIfNeeded: UInt64(bytes[0]))
|
||||
}
|
||||
|
||||
var result: T = 0
|
||||
for byte in bytes.reversed() {
|
||||
result = result << 8 | T(byte)
|
||||
}
|
||||
return result
|
||||
}
|
||||
#else
|
||||
func toInteger<T: Integer>() -> T where T: ByteConvertible, T: BitshiftOperationsType {
|
||||
if self.isEmpty {
|
||||
return 0
|
||||
}
|
||||
|
||||
var bytes = self.reversed()
|
||||
if bytes.count < MemoryLayout<T>.size {
|
||||
let paddingCount = MemoryLayout<T>.size - bytes.count
|
||||
if paddingCount > 0 {
|
||||
bytes += Array<UInt8>(repeating: 0, count: paddingCount)
|
||||
}
|
||||
}
|
||||
|
||||
if MemoryLayout<T>.size == 1 {
|
||||
return T(truncatingBitPattern: UInt64(bytes[0]))
|
||||
}
|
||||
|
||||
var result: T = 0
|
||||
for byte in bytes.reversed() {
|
||||
result = result << 8 | T(byte)
|
||||
}
|
||||
return result
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
fileprivate extension Array {
|
||||
/** split in chunks with given chunk size */
|
||||
func chunks(size chunksize: Int) -> Array<Array<Element>> {
|
||||
var words = Array<Array<Element>>()
|
||||
words.reserveCapacity(self.count / chunksize)
|
||||
for idx in stride(from: chunksize, through: self.count, by: chunksize) {
|
||||
words.append(Array(self[idx - chunksize..<idx])) // slow for large table
|
||||
}
|
||||
let reminder = self.suffix(self.count % chunksize)
|
||||
if !reminder.isEmpty {
|
||||
words.append(Array(reminder))
|
||||
}
|
||||
return words
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Data {
|
||||
var bytes: Array<UInt8> {
|
||||
return Array(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// Base64.swift
|
||||
// SwiftyBeaver (macOS)
|
||||
//
|
||||
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
|
||||
//
|
||||
#if os(Linux)
|
||||
import Foundation
|
||||
|
||||
struct InvalidBase64: Error {}
|
||||
|
||||
struct Base64 {
|
||||
static func decode(_ string: String) throws -> [UInt8] {
|
||||
return try decode([UInt8](string.utf8))
|
||||
}
|
||||
|
||||
/// Decodes a Base64 encoded String into Data
|
||||
///
|
||||
/// - throws: If the string isn't base64 encoded
|
||||
static func decode(_ string: [UInt8]) throws -> [UInt8] {
|
||||
let lookupTable: [UInt8] = [
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 62, 64, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64,
|
||||
64, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 63,
|
||||
64, 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, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64
|
||||
]
|
||||
|
||||
let remainder = string.count % 4
|
||||
let length = (string.count - remainder) / 4
|
||||
|
||||
var decoded = [UInt8]()
|
||||
decoded.reserveCapacity(length)
|
||||
|
||||
var index = 0
|
||||
var i0: UInt8 = 0
|
||||
var i1: UInt8 = 0
|
||||
var i2: UInt8 = 0
|
||||
var i3: UInt8 = 0
|
||||
|
||||
while index &+ 4 < string.count {
|
||||
i0 = lookupTable[numericCast(string[index])]
|
||||
i1 = lookupTable[numericCast(string[index &+ 1])]
|
||||
i2 = lookupTable[numericCast(string[index &+ 2])]
|
||||
i3 = lookupTable[numericCast(string[index &+ 3])]
|
||||
|
||||
if i0 > 63 || i1 > 63 || i2 > 63 || i3 > 63 {
|
||||
throw InvalidBase64()
|
||||
}
|
||||
|
||||
decoded.append(i0 << 2 | i1 >> 4)
|
||||
decoded.append(i1 << 4 | i2 >> 2)
|
||||
decoded.append(i2 << 6 | i3)
|
||||
index += 4
|
||||
}
|
||||
|
||||
if string.count &- index > 1 {
|
||||
i0 = lookupTable[numericCast(string[index])]
|
||||
i1 = lookupTable[numericCast(string[index &+ 1])]
|
||||
|
||||
if i1 > 63 {
|
||||
guard string[index] == 61 else {
|
||||
throw InvalidBase64()
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
if i2 > 63 {
|
||||
guard string[index &+ 2] == 61 else {
|
||||
throw InvalidBase64()
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
decoded.append(i0 << 2 | i1 >> 4)
|
||||
|
||||
if string.count &- index > 2 {
|
||||
i2 = lookupTable[numericCast(string[index &+ 2])]
|
||||
|
||||
if i2 > 63 {
|
||||
guard string[index &+ 2] == 61 else {
|
||||
throw InvalidBase64()
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
decoded.append(i1 << 4 | i2 >> 2)
|
||||
|
||||
if string.count &- index > 3 {
|
||||
i3 = lookupTable[numericCast(string[index &+ 3])]
|
||||
|
||||
if i3 > 63 {
|
||||
guard string[index &+ 3] == 61 else {
|
||||
throw InvalidBase64()
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
decoded.append(i2 << 6 | i3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
static func encode(_ data: [UInt8]) -> String {
|
||||
let base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
var encoded: String = ""
|
||||
|
||||
func appendCharacterFromBase(_ character: Int) {
|
||||
encoded.append(base64[base64.index(base64.startIndex, offsetBy: character)])
|
||||
}
|
||||
|
||||
func byte(_ index: Int) -> Int {
|
||||
return Int(data[index])
|
||||
}
|
||||
|
||||
let decodedBytes = data.map { Int($0) }
|
||||
|
||||
var i = 0
|
||||
|
||||
while i < decodedBytes.count - 2 {
|
||||
appendCharacterFromBase((byte(i) >> 2) & 0x3F)
|
||||
appendCharacterFromBase(((byte(i) & 0x3) << 4) | ((byte(i + 1) & 0xF0) >> 4))
|
||||
appendCharacterFromBase(((byte(i + 1) & 0xF) << 2) | ((byte(i + 2) & 0xC0) >> 6))
|
||||
appendCharacterFromBase(byte(i + 2) & 0x3F)
|
||||
i += 3
|
||||
}
|
||||
|
||||
if i < decodedBytes.count {
|
||||
appendCharacterFromBase((byte(i) >> 2) & 0x3F)
|
||||
|
||||
if i == decodedBytes.count - 1 {
|
||||
appendCharacterFromBase(((byte(i) & 0x3) << 4))
|
||||
encoded.append("=")
|
||||
} else {
|
||||
appendCharacterFromBase(((byte(i) & 0x3) << 4) | ((byte(i + 1) & 0xF0) >> 4))
|
||||
appendCharacterFromBase(((byte(i + 1) & 0xF) << 2))
|
||||
}
|
||||
|
||||
encoded.append("=")
|
||||
}
|
||||
|
||||
return encoded
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,510 @@
|
||||
//
|
||||
// BaseDestination.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger (Twitter @skreutzb) on 05.12.15.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Dispatch
|
||||
|
||||
// store operating system / platform
|
||||
#if os(iOS)
|
||||
let OS = "iOS"
|
||||
#elseif os(OSX)
|
||||
let OS = "OSX"
|
||||
#elseif os(watchOS)
|
||||
let OS = "watchOS"
|
||||
#elseif os(tvOS)
|
||||
let OS = "tvOS"
|
||||
#elseif os(Linux)
|
||||
let OS = "Linux"
|
||||
#elseif os(FreeBSD)
|
||||
let OS = "FreeBSD"
|
||||
#elseif os(Windows)
|
||||
let OS = "Windows"
|
||||
#elseif os(Android)
|
||||
let OS = "Android"
|
||||
#else
|
||||
let OS = "Unknown"
|
||||
#endif
|
||||
|
||||
/// destination which all others inherit from. do not directly use
|
||||
open class BaseDestination: Hashable, Equatable {
|
||||
|
||||
/// output format pattern, see documentation for syntax
|
||||
open var format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M"
|
||||
|
||||
/// runs in own serial background thread for better performance
|
||||
open var asynchronously = true
|
||||
|
||||
/// do not log any message which has a lower level than this one
|
||||
open var minLevel = SwiftyBeaver.Level.verbose
|
||||
|
||||
/// set custom log level words for each level
|
||||
open var levelString = LevelString()
|
||||
|
||||
/// set custom log level colors for each level
|
||||
open var levelColor = LevelColor()
|
||||
|
||||
public struct LevelString {
|
||||
public var verbose = "VERBOSE"
|
||||
public var debug = "DEBUG"
|
||||
public var info = "INFO"
|
||||
public var warning = "WARNING"
|
||||
public var error = "ERROR"
|
||||
}
|
||||
|
||||
// For a colored log level word in a logged line
|
||||
// empty on default
|
||||
public struct LevelColor {
|
||||
public var verbose = "" // silver
|
||||
public var debug = "" // green
|
||||
public var info = "" // blue
|
||||
public var warning = "" // yellow
|
||||
public var error = "" // red
|
||||
}
|
||||
|
||||
var reset = ""
|
||||
var escape = ""
|
||||
|
||||
var filters = [FilterType]()
|
||||
let formatter = DateFormatter()
|
||||
let startDate = Date()
|
||||
|
||||
// each destination class must have an own hashValue Int
|
||||
#if swift(>=4.2)
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(defaultHashValue)
|
||||
}
|
||||
#else
|
||||
lazy public var hashValue: Int = self.defaultHashValue
|
||||
#endif
|
||||
|
||||
open var defaultHashValue: Int {return 0}
|
||||
|
||||
// each destination instance must have an own serial queue to ensure serial output
|
||||
// GCD gives it a prioritization between User Initiated and Utility
|
||||
var queue: DispatchQueue? //dispatch_queue_t?
|
||||
var debugPrint = false // set to true to debug the internal filter logic of the class
|
||||
|
||||
public init() {
|
||||
let uuid = NSUUID().uuidString
|
||||
let queueLabel = "swiftybeaver-queue-" + uuid
|
||||
queue = DispatchQueue(label: queueLabel, target: queue)
|
||||
}
|
||||
|
||||
/// send / store the formatted log message to the destination
|
||||
/// returns the formatted log message for processing by inheriting method
|
||||
/// and for unit tests (nil if error)
|
||||
open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String, file: String,
|
||||
function: String, line: Int, context: Any? = nil) -> String? {
|
||||
|
||||
if format.hasPrefix("$J") {
|
||||
return messageToJSON(level, msg: msg, thread: thread,
|
||||
file: file, function: function, line: line, context: context)
|
||||
|
||||
} else {
|
||||
return formatMessage(format, level: level, msg: msg, thread: thread,
|
||||
file: file, function: function, line: line, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
public func execute(synchronously: Bool, block: @escaping () -> Void) {
|
||||
guard let queue = queue else {
|
||||
fatalError("Queue not set")
|
||||
}
|
||||
if synchronously {
|
||||
queue.sync(execute: block)
|
||||
} else {
|
||||
queue.async(execute: block)
|
||||
}
|
||||
}
|
||||
|
||||
public func executeSynchronously<T>(block: @escaping () throws -> T) rethrows -> T {
|
||||
guard let queue = queue else {
|
||||
fatalError("Queue not set")
|
||||
}
|
||||
return try queue.sync(execute: block)
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// MARK: Format
|
||||
////////////////////////////////
|
||||
|
||||
/// returns (padding length value, offset in string after padding info)
|
||||
private func parsePadding(_ text: String) -> (Int, Int) {
|
||||
// look for digits followed by a alpha character
|
||||
var s: String!
|
||||
var sign: Int = 1
|
||||
if text.firstChar == "-" {
|
||||
sign = -1
|
||||
s = String(text.suffix(from: text.index(text.startIndex, offsetBy: 1)))
|
||||
} else {
|
||||
s = text
|
||||
}
|
||||
let numStr = s.prefix { $0 >= "0" && $0 <= "9" }
|
||||
if let num = Int(String(numStr)) {
|
||||
return (sign * num, (sign == -1 ? 1 : 0) + numStr.count)
|
||||
} else {
|
||||
return (0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func paddedString(_ text: String, _ toLength: Int, truncating: Bool = false) -> String {
|
||||
if toLength > 0 {
|
||||
// Pad to the left of the string
|
||||
if text.count > toLength {
|
||||
// Hm... better to use suffix or prefix?
|
||||
return truncating ? String(text.suffix(toLength)) : text
|
||||
} else {
|
||||
return "".padding(toLength: toLength - text.count, withPad: " ", startingAt: 0) + text
|
||||
}
|
||||
} else if toLength < 0 {
|
||||
// Pad to the right of the string
|
||||
let maxLength = truncating ? -toLength : max(-toLength, text.count)
|
||||
return text.padding(toLength: maxLength, withPad: " ", startingAt: 0)
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the log message based on the format pattern
|
||||
func formatMessage(_ format: String, level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String {
|
||||
|
||||
var text = ""
|
||||
// Prepend a $I for 'ignore' or else the first character is interpreted as a format character
|
||||
// even if the format string did not start with a $.
|
||||
let phrases: [String] = ("$I" + format).components(separatedBy: "$")
|
||||
|
||||
for phrase in phrases where !phrase.isEmpty {
|
||||
let (padding, offset) = parsePadding(phrase)
|
||||
let formatCharIndex = phrase.index(phrase.startIndex, offsetBy: offset)
|
||||
let formatChar = phrase[formatCharIndex]
|
||||
let rangeAfterFormatChar = phrase.index(formatCharIndex, offsetBy: 1)..<phrase.endIndex
|
||||
let remainingPhrase = phrase[rangeAfterFormatChar]
|
||||
|
||||
switch formatChar {
|
||||
case "I": // ignore
|
||||
text += remainingPhrase
|
||||
case "L":
|
||||
text += paddedString(levelWord(level), padding) + remainingPhrase
|
||||
case "M":
|
||||
text += paddedString(msg, padding) + remainingPhrase
|
||||
case "T":
|
||||
text += paddedString(thread, padding) + remainingPhrase
|
||||
case "N":
|
||||
// name of file without suffix
|
||||
text += paddedString(fileNameWithoutSuffix(file), padding) + remainingPhrase
|
||||
case "n":
|
||||
// name of file with suffix
|
||||
text += paddedString(fileNameOfFile(file), padding) + remainingPhrase
|
||||
case "F":
|
||||
text += paddedString(function, padding) + remainingPhrase
|
||||
case "l":
|
||||
text += paddedString(String(line), padding) + remainingPhrase
|
||||
case "D":
|
||||
// start of datetime format
|
||||
#if swift(>=3.2)
|
||||
text += paddedString(formatDate(String(remainingPhrase)), padding)
|
||||
#else
|
||||
text += paddedString(formatDate(remainingPhrase), padding)
|
||||
#endif
|
||||
case "d":
|
||||
text += remainingPhrase
|
||||
case "U":
|
||||
text += paddedString(uptime(), padding) + remainingPhrase
|
||||
case "Z":
|
||||
// start of datetime format in UTC timezone
|
||||
#if swift(>=3.2)
|
||||
text += paddedString(formatDate(String(remainingPhrase), timeZone: "UTC"), padding)
|
||||
#else
|
||||
text += paddedString(formatDate(remainingPhrase, timeZone: "UTC"), padding)
|
||||
#endif
|
||||
case "z":
|
||||
text += remainingPhrase
|
||||
case "C":
|
||||
// color code ("" on default)
|
||||
text += escape + colorForLevel(level) + remainingPhrase
|
||||
case "c":
|
||||
text += reset + remainingPhrase
|
||||
case "X":
|
||||
// add the context
|
||||
if let cx = context {
|
||||
text += paddedString(String(describing: cx).trimmingCharacters(in: .whitespacesAndNewlines), padding) + remainingPhrase
|
||||
} else {
|
||||
text += paddedString("", padding) + remainingPhrase
|
||||
}
|
||||
default:
|
||||
text += phrase
|
||||
}
|
||||
}
|
||||
// right trim only
|
||||
return text.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
/// returns the log payload as optional JSON string
|
||||
func messageToJSON(_ level: SwiftyBeaver.Level, msg: String,
|
||||
thread: String, file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
var dict: [String: Any] = [
|
||||
"timestamp": Date().timeIntervalSince1970,
|
||||
"level": level.rawValue,
|
||||
"message": msg,
|
||||
"thread": thread,
|
||||
"file": file,
|
||||
"function": function,
|
||||
"line": line
|
||||
]
|
||||
if let cx = context {
|
||||
dict["context"] = cx
|
||||
}
|
||||
return jsonStringFromDict(dict)
|
||||
}
|
||||
|
||||
/// returns the string of a level
|
||||
func levelWord(_ level: SwiftyBeaver.Level) -> String {
|
||||
|
||||
var str = ""
|
||||
|
||||
switch level {
|
||||
case .debug:
|
||||
str = levelString.debug
|
||||
|
||||
case .info:
|
||||
str = levelString.info
|
||||
|
||||
case .warning:
|
||||
str = levelString.warning
|
||||
|
||||
case .error:
|
||||
str = levelString.error
|
||||
|
||||
default:
|
||||
// Verbose is default
|
||||
str = levelString.verbose
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/// returns color string for level
|
||||
func colorForLevel(_ level: SwiftyBeaver.Level) -> String {
|
||||
var color = ""
|
||||
|
||||
switch level {
|
||||
case .debug:
|
||||
color = levelColor.debug
|
||||
|
||||
case .info:
|
||||
color = levelColor.info
|
||||
|
||||
case .warning:
|
||||
color = levelColor.warning
|
||||
|
||||
case .error:
|
||||
color = levelColor.error
|
||||
|
||||
default:
|
||||
color = levelColor.verbose
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
/// returns the filename of a path
|
||||
func fileNameOfFile(_ file: String) -> String {
|
||||
let fileParts = file.components(separatedBy: "/")
|
||||
if let lastPart = fileParts.last {
|
||||
return lastPart
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// returns the filename without suffix (= file ending) of a path
|
||||
func fileNameWithoutSuffix(_ file: String) -> String {
|
||||
let fileName = fileNameOfFile(file)
|
||||
|
||||
if !fileName.isEmpty {
|
||||
let fileNameParts = fileName.components(separatedBy: ".")
|
||||
if let firstPart = fileNameParts.first {
|
||||
return firstPart
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// returns a formatted date string
|
||||
/// optionally in a given abbreviated timezone like "UTC"
|
||||
func formatDate(_ dateFormat: String, timeZone: String = "") -> String {
|
||||
if !timeZone.isEmpty {
|
||||
formatter.timeZone = TimeZone(abbreviation: timeZone)
|
||||
}
|
||||
formatter.dateFormat = dateFormat
|
||||
//let dateStr = formatter.string(from: NSDate() as Date)
|
||||
let dateStr = formatter.string(from: Date())
|
||||
return dateStr
|
||||
}
|
||||
|
||||
/// returns a uptime string
|
||||
func uptime() -> String {
|
||||
let interval = Date().timeIntervalSince(startDate)
|
||||
|
||||
let hours = Int(interval) / 3600
|
||||
let minutes = Int(interval / 60) - Int(hours * 60)
|
||||
let seconds = Int(interval) - (Int(interval / 60) * 60)
|
||||
let milliseconds = Int(interval.truncatingRemainder(dividingBy: 1) * 1000)
|
||||
|
||||
return String(format: "%0.2d:%0.2d:%0.2d.%03d", arguments: [hours, minutes, seconds, milliseconds])
|
||||
}
|
||||
|
||||
/// returns the json-encoded string value
|
||||
/// after it was encoded by jsonStringFromDict
|
||||
func jsonStringValue(_ jsonString: String?, key: String) -> String {
|
||||
guard let str = jsonString else {
|
||||
return ""
|
||||
}
|
||||
|
||||
// remove the leading {"key":" from the json string and the final }
|
||||
let offset = key.length + 5
|
||||
let endIndex = str.index(str.startIndex,
|
||||
offsetBy: str.length - 2)
|
||||
let range = str.index(str.startIndex, offsetBy: offset)..<endIndex
|
||||
#if swift(>=3.2)
|
||||
return String(str[range])
|
||||
#else
|
||||
return str[range]
|
||||
#endif
|
||||
}
|
||||
|
||||
/// turns dict into JSON-encoded string
|
||||
func jsonStringFromDict(_ dict: [String: Any]) -> String? {
|
||||
var jsonString: String?
|
||||
|
||||
// try to create JSON string
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
|
||||
jsonString = String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
print("SwiftyBeaver could not create JSON from dict.")
|
||||
}
|
||||
return jsonString
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// MARK: Filters
|
||||
////////////////////////////////
|
||||
|
||||
/// Add a filter that determines whether or not a particular message will be logged to this destination
|
||||
public func addFilter(_ filter: FilterType) {
|
||||
filters.append(filter)
|
||||
}
|
||||
|
||||
/// Remove a filter from the list of filters
|
||||
public func removeFilter(_ filter: FilterType) {
|
||||
#if swift(>=5)
|
||||
let index = filters.firstIndex {
|
||||
return ObjectIdentifier($0) == ObjectIdentifier(filter)
|
||||
}
|
||||
#else
|
||||
let index = filters.index {
|
||||
return ObjectIdentifier($0) == ObjectIdentifier(filter)
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let filterIndex = index else {
|
||||
return
|
||||
}
|
||||
|
||||
filters.remove(at: filterIndex)
|
||||
}
|
||||
|
||||
/// Answer whether the destination has any message filters
|
||||
/// returns boolean and is used to decide whether to resolve
|
||||
/// the message before invoking shouldLevelBeLogged
|
||||
func hasMessageFilters() -> Bool {
|
||||
return !getFiltersTargeting(Filter.TargetType.Message(.Equals([], true)),
|
||||
fromFilters: self.filters).isEmpty
|
||||
}
|
||||
|
||||
/// checks if level is at least minLevel or if a minLevel filter for that path does exist
|
||||
/// returns boolean and can be used to decide if a message should be logged or not
|
||||
func shouldLevelBeLogged(_ level: SwiftyBeaver.Level, path: String,
|
||||
function: String, message: String? = nil) -> Bool {
|
||||
|
||||
if filters.isEmpty {
|
||||
if level.rawValue >= minLevel.rawValue {
|
||||
if debugPrint {
|
||||
print("filters are empty and level >= minLevel")
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if debugPrint {
|
||||
print("filters are empty and level < minLevel")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let filterCheckResult = FilterValidator.validate(input: .init(filters: self.filters, level: level, path: path, function: function, message: message))
|
||||
|
||||
// Exclusion filters match if they do NOT meet the filter condition (see Filter.apply(_:) method)
|
||||
switch filterCheckResult[.excluded] {
|
||||
case .some(.someFiltersMatch):
|
||||
// Exclusion filters are present and at least one of them matches the log entry
|
||||
if debugPrint {
|
||||
print("filters are not empty and message was excluded")
|
||||
}
|
||||
return false
|
||||
case .some(.allFiltersMatch), .some(.noFiltersMatchingType), .none: break
|
||||
}
|
||||
|
||||
// If required filters exist, we should validate or invalidate the log if all of them pass or not
|
||||
switch filterCheckResult[.required] {
|
||||
case .some(.allFiltersMatch): return true
|
||||
case .some(.someFiltersMatch): return false
|
||||
case .some(.noFiltersMatchingType), .none: break
|
||||
}
|
||||
|
||||
let checkLogLevel: () -> Bool = {
|
||||
// Check if the log message's level matches or exceeds the minLevel of the destination
|
||||
return level.rawValue >= self.minLevel.rawValue
|
||||
}
|
||||
|
||||
// Non-required filters should only be applied if the log entry matches the filter condition (e.g. path)
|
||||
switch filterCheckResult[.nonRequired] {
|
||||
case .some(.allFiltersMatch): return true
|
||||
case .some(.noFiltersMatchingType), .none: return checkLogLevel()
|
||||
case .some(.someFiltersMatch(let partialMatchData)):
|
||||
if partialMatchData.fullMatchCount > 0 {
|
||||
// The log entry matches at least one filter condition and the destination's log level
|
||||
return true
|
||||
} else if partialMatchData.conditionMatchCount > 0 {
|
||||
// The log entry matches at least one filter condition, but does not match or exceed the destination's log level
|
||||
return false
|
||||
} else {
|
||||
// There is no filter with a matching filter condition. Check the destination's log level
|
||||
return checkLogLevel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFiltersTargeting(_ target: Filter.TargetType, fromFilters: [FilterType]) -> [FilterType] {
|
||||
return fromFilters.filter { filter in
|
||||
return filter.getTarget() == target
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Triggered by main flush() method on each destination. Runs in background thread.
|
||||
Use for destinations that buffer log items, implement this function to flush those
|
||||
buffers to their final destination (web server...)
|
||||
*/
|
||||
func flush() {
|
||||
// no implementation in base destination needed
|
||||
}
|
||||
}
|
||||
|
||||
public func == (lhs: BaseDestination, rhs: BaseDestination) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// ConsoleDestination.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger on 05.12.15.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class ConsoleDestination: BaseDestination {
|
||||
|
||||
/// use NSLog instead of print, default is false
|
||||
public var useNSLog = false
|
||||
/// uses colors compatible to Terminal instead of Xcode, default is false
|
||||
public var useTerminalColors: Bool = false {
|
||||
didSet {
|
||||
if useTerminalColors {
|
||||
// use Terminal colors
|
||||
reset = "\u{001b}[0m"
|
||||
escape = "\u{001b}[38;5;"
|
||||
levelColor.verbose = "251m" // silver
|
||||
levelColor.debug = "35m" // green
|
||||
levelColor.info = "38m" // blue
|
||||
levelColor.warning = "178m" // yellow
|
||||
levelColor.error = "197m" // red
|
||||
|
||||
} else {
|
||||
// use colored Emojis for better visual distinction
|
||||
// of log level for Xcode 8
|
||||
levelColor.verbose = "💜 " // silver
|
||||
levelColor.debug = "💚 " // green
|
||||
levelColor.info = "💙 " // blue
|
||||
levelColor.warning = "💛 " // yellow
|
||||
levelColor.error = "❤️ " // red
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var defaultHashValue: Int { return 1 }
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
levelColor.verbose = "💜 " // silver
|
||||
levelColor.debug = "💚 " // green
|
||||
levelColor.info = "💙 " // blue
|
||||
levelColor.warning = "💛 " // yellow
|
||||
levelColor.error = "❤️ " // red
|
||||
}
|
||||
|
||||
// print to Xcode Console. uses full base class functionality
|
||||
override public func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context)
|
||||
|
||||
if let str = formattedString {
|
||||
if useNSLog {
|
||||
#if os(Linux)
|
||||
print(str)
|
||||
#else
|
||||
NSLog("%@", str)
|
||||
#endif
|
||||
} else {
|
||||
print(str)
|
||||
}
|
||||
}
|
||||
return formattedString
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger on 13.12.17.
|
||||
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// cross-Swift compatible characters count
|
||||
var length: Int {
|
||||
return self.count
|
||||
}
|
||||
|
||||
/// cross-Swift-compatible first character
|
||||
var firstChar: Character? {
|
||||
return self.first
|
||||
}
|
||||
|
||||
/// cross-Swift-compatible last character
|
||||
var lastChar: Character? {
|
||||
return self.last
|
||||
}
|
||||
|
||||
/// cross-Swift-compatible index
|
||||
func find(_ char: Character) -> Index? {
|
||||
#if swift(>=5)
|
||||
return self.firstIndex(of: char)
|
||||
#else
|
||||
return self.index(of: char)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// FileDestination.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger on 05.12.15.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class FileDestination: BaseDestination {
|
||||
|
||||
public var logFileURL: URL?
|
||||
public var syncAfterEachWrite: Bool = false
|
||||
public var colored: Bool = false {
|
||||
didSet {
|
||||
if colored {
|
||||
// bash font color, first value is intensity, second is color
|
||||
// see http://bit.ly/1Otu3Zr & for syntax http://bit.ly/1Tp6Fw9
|
||||
// uses the 256-color table from http://bit.ly/1W1qJuH
|
||||
reset = "\u{001b}[0m"
|
||||
escape = "\u{001b}[38;5;"
|
||||
levelColor.verbose = "251m" // silver
|
||||
levelColor.debug = "35m" // green
|
||||
levelColor.info = "38m" // blue
|
||||
levelColor.warning = "178m" // yellow
|
||||
levelColor.error = "197m" // red
|
||||
} else {
|
||||
reset = ""
|
||||
escape = ""
|
||||
levelColor.verbose = ""
|
||||
levelColor.debug = ""
|
||||
levelColor.info = ""
|
||||
levelColor.warning = ""
|
||||
levelColor.error = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var defaultHashValue: Int {return 2}
|
||||
let fileManager = FileManager.default
|
||||
|
||||
public init(logFileURL: URL? = nil) {
|
||||
if let logFileURL = logFileURL {
|
||||
self.logFileURL = logFileURL
|
||||
super.init()
|
||||
return
|
||||
}
|
||||
|
||||
// platform-dependent logfile directory default
|
||||
var baseURL: URL?
|
||||
#if os(OSX)
|
||||
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
baseURL = url
|
||||
// try to use ~/Library/Caches/APP NAME instead of ~/Library/Caches
|
||||
if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String {
|
||||
do {
|
||||
if let appURL = baseURL?.appendingPathComponent(appName, isDirectory: true) {
|
||||
try fileManager.createDirectory(at: appURL,
|
||||
withIntermediateDirectories: true, attributes: nil)
|
||||
baseURL = appURL
|
||||
}
|
||||
} catch {
|
||||
print("Warning! Could not create folder /Library/Caches/\(appName)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
#if os(Linux)
|
||||
baseURL = URL(fileURLWithPath: "/var/cache")
|
||||
#else
|
||||
// iOS, watchOS, etc. are using the caches directory
|
||||
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
baseURL = url
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if let baseURL = baseURL {
|
||||
self.logFileURL = baseURL.appendingPathComponent("swiftybeaver.log", isDirectory: false)
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
// append to file. uses full base class functionality
|
||||
override public func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context)
|
||||
|
||||
if let str = formattedString {
|
||||
_ = saveToFile(str: str)
|
||||
}
|
||||
return formattedString
|
||||
}
|
||||
|
||||
/// appends a string as line to a file.
|
||||
/// returns boolean about success
|
||||
func saveToFile(str: String) -> Bool {
|
||||
guard let url = logFileURL else { return false }
|
||||
|
||||
let line = str + "\n"
|
||||
guard let data = line.data(using: String.Encoding.utf8) else { return false }
|
||||
|
||||
do {
|
||||
if fileManager.fileExists(atPath: url.path) == false {
|
||||
|
||||
let directoryURL = url.deletingLastPathComponent()
|
||||
if fileManager.fileExists(atPath: directoryURL.path) == false {
|
||||
try fileManager.createDirectory(
|
||||
at: directoryURL,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
}
|
||||
fileManager.createFile(atPath: url.path, contents: nil)
|
||||
|
||||
#if os(iOS) || os(watchOS)
|
||||
if #available(iOS 10.0, watchOS 3.0, *) {
|
||||
var attributes = try fileManager.attributesOfItem(atPath: url.path)
|
||||
attributes[FileAttributeKey.protectionKey] = FileProtectionType.none
|
||||
try fileManager.setAttributes(attributes, ofItemAtPath: url.path)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
write(data: data, to: url)
|
||||
|
||||
return true
|
||||
} catch {
|
||||
print("SwiftyBeaver File Destination could not write to file \(url).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func write(data: Data, to url: URL) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
var error: NSError?
|
||||
coordinator.coordinate(writingItemAt: url, error: &error) { url in
|
||||
do {
|
||||
let fileHandle = try FileHandle(forWritingTo: url)
|
||||
fileHandle.seekToEndOfFile()
|
||||
fileHandle.write(data)
|
||||
if syncAfterEachWrite {
|
||||
fileHandle.synchronizeFile()
|
||||
}
|
||||
fileHandle.closeFile()
|
||||
} catch {
|
||||
print("SwiftyBeaver File Destination could not write to file \(url).")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
print("Failed writing file with error: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
|
||||
/// deletes log file.
|
||||
/// returns true if file was removed or does not exist, false otherwise
|
||||
public func deleteLogFile() -> Bool {
|
||||
guard let url = logFileURL, fileManager.fileExists(atPath: url.path) == true else { return true }
|
||||
do {
|
||||
try fileManager.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
print("SwiftyBeaver File Destination could not remove file \(url).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
//
|
||||
// Filter.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Jeff Roberts on 5/31/16.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// FilterType is a protocol that describes something that determines
|
||||
/// whether or not a message gets logged. A filter answers a Bool when it
|
||||
/// is applied to a value. If the filter passes, it shall return true,
|
||||
/// false otherwise.
|
||||
///
|
||||
/// A filter must contain a target, which identifies what it filters against
|
||||
/// A filter can be required meaning that all required filters against a specific
|
||||
/// target must pass in order for the message to be logged.
|
||||
public protocol FilterType : class {
|
||||
func apply(_ value: Any) -> Bool
|
||||
func getTarget() -> Filter.TargetType
|
||||
func isRequired() -> Bool
|
||||
func isExcluded() -> Bool
|
||||
func reachedMinLevel(_ level: SwiftyBeaver.Level) -> Bool
|
||||
}
|
||||
|
||||
/// Filters is syntactic sugar used to easily construct filters
|
||||
public class Filters {
|
||||
public static let Path = PathFilterFactory.self
|
||||
public static let Function = FunctionFilterFactory.self
|
||||
public static let Message = MessageFilterFactory.self
|
||||
}
|
||||
|
||||
/// Filter is an abstract base class for other filters
|
||||
public class Filter {
|
||||
public enum TargetType {
|
||||
case Path(Filter.ComparisonType)
|
||||
case Function(Filter.ComparisonType)
|
||||
case Message(Filter.ComparisonType)
|
||||
}
|
||||
|
||||
public enum ComparisonType {
|
||||
case StartsWith([String], Bool)
|
||||
case Contains([String], Bool)
|
||||
case Excludes([String], Bool)
|
||||
case EndsWith([String], Bool)
|
||||
case Equals([String], Bool)
|
||||
case Custom((String) -> Bool)
|
||||
}
|
||||
|
||||
let targetType: Filter.TargetType
|
||||
let required: Bool
|
||||
let minLevel: SwiftyBeaver.Level
|
||||
|
||||
public init(_ target: Filter.TargetType, required: Bool, minLevel: SwiftyBeaver.Level) {
|
||||
self.targetType = target
|
||||
self.required = required
|
||||
self.minLevel = minLevel
|
||||
}
|
||||
|
||||
public func getTarget() -> Filter.TargetType {
|
||||
return self.targetType
|
||||
}
|
||||
|
||||
public func isRequired() -> Bool {
|
||||
return self.required
|
||||
}
|
||||
|
||||
public func isExcluded() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// returns true of set minLevel is >= as given level
|
||||
public func reachedMinLevel(_ level: SwiftyBeaver.Level) -> Bool {
|
||||
//print("checking if given level \(level) >= \(minLevel)")
|
||||
return level.rawValue >= minLevel.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// CompareFilter is a FilterType that can filter based upon whether a target
|
||||
/// starts with, contains or ends with a specific string. CompareFilters can be
|
||||
/// case sensitive.
|
||||
public class CompareFilter: Filter, FilterType {
|
||||
|
||||
private var filterComparisonType: Filter.ComparisonType?
|
||||
|
||||
override public init(_ target: Filter.TargetType, required: Bool, minLevel: SwiftyBeaver.Level) {
|
||||
super.init(target, required: required, minLevel: minLevel)
|
||||
|
||||
let comparisonType: Filter.ComparisonType?
|
||||
switch self.getTarget() {
|
||||
case let .Function(comparison):
|
||||
comparisonType = comparison
|
||||
|
||||
case let .Path(comparison):
|
||||
comparisonType = comparison
|
||||
|
||||
case let .Message(comparison):
|
||||
comparisonType = comparison
|
||||
|
||||
/*default:
|
||||
comparisonType = nil*/
|
||||
}
|
||||
self.filterComparisonType = comparisonType
|
||||
}
|
||||
|
||||
public func apply(_ value: Any) -> Bool {
|
||||
guard let value = value as? String else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let filterComparisonType = self.filterComparisonType else {
|
||||
return false
|
||||
}
|
||||
|
||||
let matches: Bool
|
||||
switch filterComparisonType {
|
||||
case let .Contains(strings, caseSensitive):
|
||||
matches = !strings.filter { string in
|
||||
return caseSensitive ? value.contains(string) :
|
||||
value.lowercased().contains(string.lowercased())
|
||||
}.isEmpty
|
||||
|
||||
case let .Excludes(strings, caseSensitive):
|
||||
matches = !strings.filter { string in
|
||||
return caseSensitive ? !value.contains(string) :
|
||||
!value.lowercased().contains(string.lowercased())
|
||||
}.isEmpty
|
||||
|
||||
case let .StartsWith(strings, caseSensitive):
|
||||
matches = !strings.filter { string in
|
||||
return caseSensitive ? value.hasPrefix(string) :
|
||||
value.lowercased().hasPrefix(string.lowercased())
|
||||
}.isEmpty
|
||||
|
||||
case let .EndsWith(strings, caseSensitive):
|
||||
matches = !strings.filter { string in
|
||||
return caseSensitive ? value.hasSuffix(string) :
|
||||
value.lowercased().hasSuffix(string.lowercased())
|
||||
}.isEmpty
|
||||
|
||||
case let .Equals(strings, caseSensitive):
|
||||
matches = !strings.filter { string in
|
||||
return caseSensitive ? value == string :
|
||||
value.lowercased() == string.lowercased()
|
||||
}.isEmpty
|
||||
case let .Custom(predicate):
|
||||
matches = predicate(value)
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
override public func isExcluded() -> Bool {
|
||||
guard let filterComparisonType = self.filterComparisonType else { return false }
|
||||
|
||||
switch filterComparisonType {
|
||||
case .Excludes:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Syntactic sugar for creating a function comparison filter
|
||||
public class FunctionFilterFactory {
|
||||
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Function(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func contains(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Function(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Function(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Function(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func equals(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Function(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
|
||||
return CompareFilter(.Function(.Custom(filterPredicate)), required: required, minLevel: minLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Syntactic sugar for creating a message comparison filter
|
||||
public class MessageFilterFactory {
|
||||
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Message(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func contains(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Message(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Message(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Message(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func equals(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Message(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
|
||||
return CompareFilter(.Message(.Custom(filterPredicate)), required: required, minLevel: minLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Syntactic sugar for creating a path comparison filter
|
||||
public class PathFilterFactory {
|
||||
public static func startsWith(_ prefixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Path(.StartsWith(prefixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func contains(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Path(.Contains(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func excludes(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Path(.Excludes(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func endsWith(_ suffixes: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Path(.EndsWith(suffixes, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func equals(_ strings: String..., caseSensitive: Bool = false,
|
||||
required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose) -> FilterType {
|
||||
return CompareFilter(.Path(.Equals(strings, caseSensitive)), required: required, minLevel: minLevel)
|
||||
}
|
||||
|
||||
public static func custom(required: Bool = false, minLevel: SwiftyBeaver.Level = .verbose, filterPredicate: @escaping (String) -> Bool) -> FilterType {
|
||||
return CompareFilter(.Path(.Custom(filterPredicate)), required: required, minLevel: minLevel)
|
||||
}
|
||||
}
|
||||
|
||||
extension Filter.TargetType: Equatable {
|
||||
}
|
||||
|
||||
// The == does not compare associated values for each enum. Instead == evaluates to true
|
||||
// if both enums are the same "types", ignoring the associated values of each enum
|
||||
public func == (lhs: Filter.TargetType, rhs: Filter.TargetType) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
|
||||
case (.Path, .Path):
|
||||
return true
|
||||
|
||||
case (.Function, .Function):
|
||||
return true
|
||||
|
||||
case (.Message, .Message):
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// FilterValidator.swift
|
||||
// SwiftyBeaver (iOS)
|
||||
//
|
||||
// Created by Felix Lisczyk on 07.07.19.
|
||||
// Copyright © 2019 Sebastian Kreutzberger. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// FilterValidator is a utility class used by BaseDestination.
|
||||
/// It encapsulates the filtering logic for excluded, required
|
||||
/// and non-required filters.
|
||||
///
|
||||
/// FilterValidator evaluates a set of filters for a single log
|
||||
/// entry. It determines if these filters apply to the log entry
|
||||
/// based on their condition (path, function, message) and their
|
||||
/// minimum log level.
|
||||
|
||||
struct FilterValidator {
|
||||
|
||||
// These are the different filter types that the user can set
|
||||
enum ValidationType: CaseIterable {
|
||||
case excluded
|
||||
case required
|
||||
case nonRequired
|
||||
|
||||
func apply(to filters: [FilterType]) -> [FilterType] {
|
||||
switch self {
|
||||
case .excluded:
|
||||
return filters.filter { $0.isExcluded() }
|
||||
case .required:
|
||||
return filters.filter { $0.isRequired() && !$0.isExcluded() }
|
||||
case .nonRequired:
|
||||
return filters.filter { !$0.isRequired() && !$0.isExcluded() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper object for input parameters
|
||||
struct Input {
|
||||
let filters: [FilterType]
|
||||
let level: SwiftyBeaver.Level
|
||||
let path: String
|
||||
let function: String
|
||||
let message: String?
|
||||
}
|
||||
|
||||
// Result wrapper object
|
||||
enum Result {
|
||||
case allFiltersMatch // All filters fully match the log entry (condition + minimum log level)
|
||||
case someFiltersMatch(PartialMatchData) // Only some filters fully match the log entry (condition + minimum log level)
|
||||
case noFiltersMatchingType // There are no filters set for a particular type (excluded, required, nonRequired)
|
||||
|
||||
struct PartialMatchData {
|
||||
let fullMatchCount: Int // Number of filters that match both the condition and the minimum log level of the log entry
|
||||
let conditionMatchCount: Int // Number of filters that match ONLY the condition of the log entry (path, function, message)
|
||||
let logLevelMatchCount: Int // Number of filters that match ONLY the minimum log level of the log entry
|
||||
}
|
||||
}
|
||||
|
||||
static func validate(input: Input, for types: [ValidationType] = ValidationType.allCases) -> [ValidationType: Result] {
|
||||
var results = [ValidationType: Result]()
|
||||
for type in types {
|
||||
let filtersToValidate = type.apply(to: input.filters)
|
||||
|
||||
if filtersToValidate.isEmpty {
|
||||
// There are no filters set for this particular type
|
||||
results[type] = .noFiltersMatchingType
|
||||
} else {
|
||||
var fullMatchCount: Int = 0
|
||||
var conditionMatchCount: Int = 0
|
||||
var logLevelMatchCount: Int = 0
|
||||
|
||||
for filter in filtersToValidate {
|
||||
let filterMatchesCondition = self.filterMatchesCondition(filter, level: input.level, path: input.path, function: input.function, message: input.message)
|
||||
let filterMatchesMinLogLevel = self.filterMatchesMinLogLevel(filter, level: input.level)
|
||||
|
||||
switch (filterMatchesCondition, filterMatchesMinLogLevel) {
|
||||
// Filter matches both the condition and the minimum log level
|
||||
case (true, true): fullMatchCount += 1
|
||||
// Filter matches only the condition (path, function, message)
|
||||
case (true, false): conditionMatchCount += 1
|
||||
// Filter matches only the minimum log level
|
||||
case (false, true): logLevelMatchCount += 1
|
||||
// Filter does not match the condition nor the minimum log level
|
||||
case (false, false): break
|
||||
}
|
||||
}
|
||||
|
||||
if filtersToValidate.count == fullMatchCount {
|
||||
// All filters fully match the log entry
|
||||
results[type] = .allFiltersMatch
|
||||
} else {
|
||||
// Only some filters match the log entry
|
||||
results[type] = .someFiltersMatch(.init(fullMatchCount: fullMatchCount, conditionMatchCount: conditionMatchCount, logLevelMatchCount: logLevelMatchCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private static func filterMatchesCondition(_ filter: FilterType, level: SwiftyBeaver.Level,
|
||||
path: String, function: String, message: String?) -> Bool {
|
||||
let passes: Bool
|
||||
|
||||
switch filter.getTarget() {
|
||||
case .Path(_):
|
||||
passes = filter.apply(path)
|
||||
|
||||
case .Function(_):
|
||||
passes = filter.apply(function)
|
||||
|
||||
case .Message(_):
|
||||
guard let message = message else {
|
||||
return false
|
||||
}
|
||||
|
||||
passes = filter.apply(message)
|
||||
}
|
||||
|
||||
return passes
|
||||
}
|
||||
|
||||
private static func filterMatchesMinLogLevel(_ filter: FilterType, level: SwiftyBeaver.Level) -> Bool {
|
||||
return filter.reachedMinLevel(level)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// GoogleCloudDestination.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Copyright © 2017 Sebastian Kreutzberger. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class GoogleCloudDestination: BaseDestination {
|
||||
|
||||
private let serviceName: String
|
||||
|
||||
public init(serviceName: String) {
|
||||
self.serviceName = serviceName
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public var asynchronously: Bool {
|
||||
get {
|
||||
return false
|
||||
}
|
||||
set {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override public func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
|
||||
let reportLocation: [String: Any] = ["filePath": file, "lineNumber": line, "functionName": function]
|
||||
var gcpContext: [String: Any] = ["reportLocation": reportLocation]
|
||||
if let context = context as? [String: Any] {
|
||||
if let httpRequestContext = context["httpRequest"] as? [String: Any] {
|
||||
gcpContext["httpRequest"] = httpRequestContext
|
||||
}
|
||||
|
||||
if let user = context["user"] as? String {
|
||||
gcpContext["user"] = user
|
||||
}
|
||||
}
|
||||
|
||||
let gcpJSON: [String: Any] = [
|
||||
"serviceContext": [
|
||||
"service": serviceName
|
||||
],
|
||||
"message": msg,
|
||||
"severity": level.severity,
|
||||
"context": gcpContext
|
||||
]
|
||||
|
||||
let finalLogString: String
|
||||
|
||||
do {
|
||||
finalLogString = try jsonString(obj: gcpJSON)
|
||||
} catch {
|
||||
let uncrashableLogString = "{\"context\":{\"reportLocation\":{\"filePath\": \"\(file)\"" +
|
||||
",\"functionName\":\"\(function)\"" +
|
||||
",\"lineNumber\":\(line)},\"severity\"" +
|
||||
":\"CRITICAL\",\"message\":\"Error encoding " +
|
||||
"JSON log entry. You may be losing log messages!\"}"
|
||||
finalLogString = uncrashableLogString.description
|
||||
}
|
||||
print(finalLogString)
|
||||
return finalLogString
|
||||
}
|
||||
|
||||
private func jsonString(obj: Dictionary<String, Any>) throws -> String {
|
||||
let json = try JSONSerialization.data(withJSONObject: obj, options: [])
|
||||
guard let string = String(data: json, encoding: .utf8) else {
|
||||
throw GCPError.serialization
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
|
||||
extension SwiftyBeaver.Level {
|
||||
|
||||
/// Verbose is reported as Debug to GCP.
|
||||
/// Recommend you don't bother using it.
|
||||
var severity: String {
|
||||
switch self {
|
||||
// There is only one level below "Debug": "Default", which becomes "Any" and is considered as a potential error as well
|
||||
case .verbose: return "DEBUG"
|
||||
case .debug: return "DEBUG"
|
||||
case .info: return "INFO"
|
||||
case .warning: return "WARNING"
|
||||
case .error: return "ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum GCPError: Error {
|
||||
case serialization
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//
|
||||
// SBPlatformDestination
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger on 22.01.16.
|
||||
// Copyright © 2016 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
// platform-dependent import frameworks to get device details
|
||||
// valid values for os(): OSX, iOS, watchOS, tvOS, Linux
|
||||
// in Swift 3 the following were added: FreeBSD, Windows, Android
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
var DEVICE_MODEL: String {
|
||||
get {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
}
|
||||
#else
|
||||
let DEVICE_MODEL = ""
|
||||
#endif
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
var DEVICE_NAME = UIDevice.current.name
|
||||
#else
|
||||
// under watchOS UIDevice is not existing, http://apple.co/26ch5J1
|
||||
let DEVICE_NAME = ""
|
||||
#endif
|
||||
|
||||
public class SBPlatformDestination: BaseDestination {
|
||||
|
||||
public var appID = ""
|
||||
public var appSecret = ""
|
||||
public var encryptionKey = ""
|
||||
public var analyticsUserName = "" // user email, ID, name, etc.
|
||||
public var analyticsUUID: String { return uuid }
|
||||
|
||||
// when to send to server
|
||||
public struct SendingPoints {
|
||||
public var verbose = 0
|
||||
public var debug = 1
|
||||
public var info = 5
|
||||
public var warning = 8
|
||||
public var error = 10
|
||||
public var threshold = 10 // send to server if points reach that value
|
||||
}
|
||||
public var sendingPoints = SendingPoints()
|
||||
public var showNSLog = false // executes toNSLog statements to debug the class
|
||||
var points = 0
|
||||
|
||||
public var serverURL = URL(string: "https://api.swiftybeaver.com/api/entries/") // optional
|
||||
public var entriesFileURL = URL(fileURLWithPath: "") // not optional
|
||||
public var sendingFileURL = URL(fileURLWithPath: "")
|
||||
public var analyticsFileURL = URL(fileURLWithPath: "")
|
||||
|
||||
private let minAllowedThreshold = 1 // over-rules SendingPoints.Threshold
|
||||
private let maxAllowedThreshold = 1000 // over-rules SendingPoints.Threshold
|
||||
private var sendingInProgress = false
|
||||
private var initialSending = true
|
||||
|
||||
// analytics
|
||||
var uuid = ""
|
||||
|
||||
// destination
|
||||
override public var defaultHashValue: Int {return 3}
|
||||
let fileManager = FileManager.default
|
||||
let isoDateFormatter = DateFormatter()
|
||||
|
||||
/// init platform with default internal filenames
|
||||
public init(appID: String, appSecret: String, encryptionKey: String,
|
||||
serverURL: URL? = URL(string: "https://api.swiftybeaver.com/api/entries/"),
|
||||
entriesFileName: String = "sbplatform_entries.json",
|
||||
sendingfileName: String = "sbplatform_entries_sending.json",
|
||||
analyticsFileName: String = "sbplatform_analytics.json") {
|
||||
super.init()
|
||||
self.serverURL = serverURL
|
||||
self.appID = appID
|
||||
self.appSecret = appSecret
|
||||
self.encryptionKey = encryptionKey
|
||||
|
||||
// setup where to write the json files
|
||||
var baseURL: URL?
|
||||
#if os(OSX)
|
||||
if let url = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
||||
baseURL = url
|
||||
// try to use ~/Library/Application Support/APP NAME instead of ~/Library/Application Support
|
||||
if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String {
|
||||
do {
|
||||
if let appURL = baseURL?.appendingPathComponent(appName, isDirectory: true) {
|
||||
try fileManager.createDirectory(at: appURL,
|
||||
withIntermediateDirectories: true, attributes: nil)
|
||||
baseURL = appURL
|
||||
}
|
||||
} catch {
|
||||
// it is too early in the class lifetime to be able to use toNSLog()
|
||||
print("Warning! Could not create folder ~/Library/Application Support/\(appName).")
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
#if os(tvOS)
|
||||
// tvOS can just use the caches directory
|
||||
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
baseURL = url
|
||||
}
|
||||
#elseif os(Linux)
|
||||
// Linux is using /var/cache
|
||||
let baseDir = "/var/cache/"
|
||||
entriesFileURL = URL(fileURLWithPath: baseDir + entriesFileName)
|
||||
sendingFileURL = URL(fileURLWithPath: baseDir + sendingfileName)
|
||||
analyticsFileURL = URL(fileURLWithPath: baseDir + analyticsFileName)
|
||||
#else
|
||||
// iOS and watchOS are using the app’s document directory
|
||||
if let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
baseURL = url
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if os(Linux)
|
||||
// get, update loaded and save analytics data to file on start
|
||||
let dict = analytics(analyticsFileURL, update: true)
|
||||
_ = saveDictToFile(dict, url: analyticsFileURL)
|
||||
#else
|
||||
if let baseURL = baseURL {
|
||||
// is just set for everything but not Linux
|
||||
entriesFileURL = baseURL.appendingPathComponent(entriesFileName,
|
||||
isDirectory: false)
|
||||
sendingFileURL = baseURL.appendingPathComponent(sendingfileName,
|
||||
isDirectory: false)
|
||||
analyticsFileURL = baseURL.appendingPathComponent(analyticsFileName,
|
||||
isDirectory: false)
|
||||
|
||||
// get, update loaded and save analytics data to file on start
|
||||
let dict = analytics(analyticsFileURL, update: true)
|
||||
_ = saveDictToFile(dict, url: analyticsFileURL)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// append to file, each line is a JSON dict
|
||||
override public func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
|
||||
var jsonString: String?
|
||||
|
||||
let dict: [String: Any] = [
|
||||
"timestamp": Date().timeIntervalSince1970,
|
||||
"level": level.rawValue,
|
||||
"message": msg,
|
||||
"thread": thread,
|
||||
"fileName": file.components(separatedBy: "/").last!,
|
||||
"function": function,
|
||||
"line": line]
|
||||
|
||||
jsonString = jsonStringFromDict(dict)
|
||||
|
||||
if let str = jsonString {
|
||||
toNSLog("saving '\(msg)' to \(entriesFileURL)")
|
||||
_ = saveToFile(str, url: entriesFileURL)
|
||||
//toNSLog(entriesFileURL.path!)
|
||||
|
||||
// now decide if the stored log entries should be sent to the server
|
||||
// add level points to current points amount and send to server if threshold is hit
|
||||
let newPoints = sendingPointsForLevel(level)
|
||||
points += newPoints
|
||||
toNSLog("current sending points: \(points)")
|
||||
|
||||
if (points >= sendingPoints.threshold && points >= minAllowedThreshold) || points > maxAllowedThreshold {
|
||||
toNSLog("\(points) points is >= threshold")
|
||||
// above threshold, send to server
|
||||
sendNow()
|
||||
|
||||
} else if initialSending {
|
||||
initialSending = false
|
||||
// first logging at this session
|
||||
// send if json file still contains old log entries
|
||||
if let logEntries = logsFromFile(entriesFileURL) {
|
||||
let lines = logEntries.count
|
||||
if lines > 1 {
|
||||
var msg = "initialSending: \(points) points is below threshold "
|
||||
msg += "but json file already has \(lines) lines."
|
||||
toNSLog(msg)
|
||||
sendNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return jsonString
|
||||
}
|
||||
|
||||
// MARK: Send-to-Server Logic
|
||||
|
||||
/// does a (manual) sending attempt of all unsent log entries to SwiftyBeaver Platform
|
||||
public func sendNow() {
|
||||
|
||||
if sendFileExists() {
|
||||
toNSLog("reset points to 0")
|
||||
points = 0
|
||||
} else {
|
||||
if !renameJsonToSendFile() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !sendingInProgress {
|
||||
sendingInProgress = true
|
||||
//let (jsonString, lines) = logsFromFile(sendingFileURL)
|
||||
var lines = 0
|
||||
|
||||
guard let logEntries = logsFromFile(sendingFileURL) else {
|
||||
sendingInProgress = false
|
||||
return
|
||||
}
|
||||
|
||||
lines = logEntries.count
|
||||
|
||||
if lines > 0 {
|
||||
var payload = [String: Any]()
|
||||
// merge device and analytics dictionaries
|
||||
let deviceDetailsDict = deviceDetails()
|
||||
|
||||
var analyticsDict = analytics(analyticsFileURL)
|
||||
|
||||
for key in deviceDetailsDict.keys {
|
||||
analyticsDict[key] = deviceDetailsDict[key]
|
||||
}
|
||||
payload["device"] = analyticsDict
|
||||
payload["entries"] = logEntries
|
||||
|
||||
if let str = jsonStringFromDict(payload) {
|
||||
//toNSLog(str) // uncomment to see full payload
|
||||
toNSLog("Encrypting \(lines) log entries ...")
|
||||
if let encryptedStr = encrypt(str) {
|
||||
var msg = "Sending \(lines) encrypted log entries "
|
||||
msg += "(\(encryptedStr.length) chars) to server ..."
|
||||
toNSLog(msg)
|
||||
sendToServerAsync(encryptedStr) { ok, _ in
|
||||
|
||||
self.toNSLog("Sent \(lines) encrypted log entries to server, received ok: \(ok)")
|
||||
if ok {
|
||||
_ = self.deleteFile(self.sendingFileURL)
|
||||
}
|
||||
self.sendingInProgress = false
|
||||
self.points = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendingInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// sends a string to the SwiftyBeaver Platform server, returns ok if status 200 and HTTP status
|
||||
func sendToServerAsync(_ str: String?, complete: @escaping (_ ok: Bool, _ status: Int) -> Void) {
|
||||
|
||||
let timeout = 10.0
|
||||
|
||||
if let payload = str, let queue = self.queue, let serverURL = serverURL {
|
||||
|
||||
// create operation queue which uses current serial queue of destination
|
||||
let operationQueue = OperationQueue()
|
||||
operationQueue.underlyingQueue = queue
|
||||
|
||||
let session = URLSession(configuration:
|
||||
URLSessionConfiguration.default,
|
||||
delegate: nil, delegateQueue: operationQueue)
|
||||
|
||||
toNSLog("assembling request ...")
|
||||
|
||||
// assemble request
|
||||
var request = URLRequest(url: serverURL,
|
||||
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
|
||||
timeoutInterval: timeout)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
// basic auth header (just works on Linux for Swift 3.1+, macOS is fine)
|
||||
guard let credentials = "\(appID):\(appSecret)".data(using: String.Encoding.utf8) else {
|
||||
toNSLog("Error! Could not set basic auth header")
|
||||
return complete(false, 0)
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
let base64Credentials = Base64.encode([UInt8](credentials))
|
||||
#else
|
||||
let base64Credentials = credentials.base64EncodedString(options: [])
|
||||
#endif
|
||||
request.setValue("Basic \(base64Credentials)", forHTTPHeaderField: "Authorization")
|
||||
//toNSLog("\nrequest:")
|
||||
//print(request)
|
||||
|
||||
// POST parameters
|
||||
let params = ["payload": payload]
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
|
||||
} catch {
|
||||
toNSLog("Error! Could not create JSON for server payload.")
|
||||
return complete(false, 0)
|
||||
}
|
||||
toNSLog("sending params: \(params)")
|
||||
toNSLog("sending ...")
|
||||
|
||||
sendingInProgress = true
|
||||
|
||||
// send request async to server on destination queue
|
||||
let task = session.dataTask(with: request) { _, response, error in
|
||||
var ok = false
|
||||
var status = 0
|
||||
self.toNSLog("received response from server")
|
||||
|
||||
if let error = error {
|
||||
// an error did occur
|
||||
self.toNSLog("Error! Could not send entries to server. \(error)")
|
||||
} else {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
status = response.statusCode
|
||||
if status == 200 {
|
||||
// all went well, entries were uploaded to server
|
||||
ok = true
|
||||
} else {
|
||||
// status code was not 200
|
||||
var msg = "Error! Sending entries to server failed "
|
||||
msg += "with status code \(status)"
|
||||
self.toNSLog(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return complete(ok, status)
|
||||
}
|
||||
task.resume()
|
||||
session.finishTasksAndInvalidate()
|
||||
//while true {} // commenting this line causes a crash on Linux unit tests?!?
|
||||
}
|
||||
}
|
||||
|
||||
/// returns sending points based on level
|
||||
func sendingPointsForLevel(_ level: SwiftyBeaver.Level) -> Int {
|
||||
|
||||
switch level {
|
||||
case .debug:
|
||||
return sendingPoints.debug
|
||||
case .info:
|
||||
return sendingPoints.info
|
||||
case .warning:
|
||||
return sendingPoints.warning
|
||||
case .error:
|
||||
return sendingPoints.error
|
||||
default:
|
||||
return sendingPoints.verbose
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: File Handling
|
||||
|
||||
/// appends a string as line to a file.
|
||||
/// returns boolean about success
|
||||
func saveToFile(_ str: String, url: URL, overwrite: Bool = false) -> Bool {
|
||||
do {
|
||||
if fileManager.fileExists(atPath: url.path) == false || overwrite {
|
||||
// create file if not existing
|
||||
let line = str + "\n"
|
||||
try line.write(to: url, atomically: true, encoding: String.Encoding.utf8)
|
||||
} else {
|
||||
// append to end of file
|
||||
let fileHandle = try FileHandle(forWritingTo: url)
|
||||
_ = fileHandle.seekToEndOfFile()
|
||||
let line = str + "\n"
|
||||
if let data = line.data(using: String.Encoding.utf8) {
|
||||
fileHandle.write(data)
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
toNSLog("Error! Could not write to file \(url).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func sendFileExists() -> Bool {
|
||||
return fileManager.fileExists(atPath: sendingFileURL.path)
|
||||
}
|
||||
|
||||
func renameJsonToSendFile() -> Bool {
|
||||
do {
|
||||
try fileManager.moveItem(at: entriesFileURL, to: sendingFileURL)
|
||||
return true
|
||||
} catch {
|
||||
toNSLog("SwiftyBeaver Platform Destination could not rename json file.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// returns optional array of log dicts from a file which has 1 json string per line
|
||||
func logsFromFile(_ url: URL) -> [[String: Any]]? {
|
||||
var lines = 0
|
||||
do {
|
||||
// try to read file, decode every JSON line and put dict from each line in array
|
||||
let fileContent = try String(contentsOfFile: url.path, encoding: .utf8)
|
||||
let linesArray = fileContent.components(separatedBy: "\n")
|
||||
var dicts = [[String: Any]()] // array of dictionaries
|
||||
for lineJSON in linesArray {
|
||||
lines += 1
|
||||
if lineJSON.firstChar == "{" && lineJSON.lastChar == "}" {
|
||||
// try to parse json string into dict
|
||||
if let data = lineJSON.data(using: .utf8) {
|
||||
do {
|
||||
if let dict = try JSONSerialization.jsonObject(with: data,
|
||||
options: .mutableContainers) as? [String: Any] {
|
||||
if !dict.isEmpty {
|
||||
dicts.append(dict)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
var msg = "Error! Could not parse "
|
||||
msg += "line \(lines) in file \(url)."
|
||||
toNSLog(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dicts.removeFirst()
|
||||
return dicts
|
||||
} catch {
|
||||
toNSLog("Error! Could not read file \(url).")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// returns AES-256 CBC encrypted optional string
|
||||
func encrypt(_ str: String) -> String? {
|
||||
return AES256CBC.encryptString(str, password: encryptionKey)
|
||||
}
|
||||
|
||||
/// Delete file to get started again
|
||||
func deleteFile(_ url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
toNSLog("Warning! Could not delete file \(url).")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Device & Analytics
|
||||
|
||||
// returns dict with device details. Amount depends on platform
|
||||
func deviceDetails() -> [String: String] {
|
||||
var details = [String: String]()
|
||||
|
||||
details["os"] = OS
|
||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
// becomes for example 10.11.2 for El Capitan
|
||||
var osVersionStr = String(osVersion.majorVersion)
|
||||
osVersionStr += "." + String(osVersion.minorVersion)
|
||||
osVersionStr += "." + String(osVersion.patchVersion)
|
||||
details["osVersion"] = osVersionStr
|
||||
details["hostName"] = ProcessInfo.processInfo.hostName
|
||||
details["deviceName"] = ""
|
||||
details["deviceModel"] = ""
|
||||
|
||||
if DEVICE_NAME != "" {
|
||||
details["deviceName"] = DEVICE_NAME
|
||||
}
|
||||
if DEVICE_MODEL != "" {
|
||||
details["deviceModel"] = DEVICE_MODEL
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
/// returns (updated) analytics dict, optionally loaded from file.
|
||||
func analytics(_ url: URL, update: Bool = false) -> [String: Any] {
|
||||
|
||||
var dict = [String: Any]()
|
||||
let now = NSDate().timeIntervalSince1970
|
||||
|
||||
uuid = NSUUID().uuidString
|
||||
dict["uuid"] = uuid
|
||||
dict["firstStart"] = now
|
||||
dict["lastStart"] = now
|
||||
dict["starts"] = 1
|
||||
dict["userName"] = analyticsUserName
|
||||
dict["firstAppVersion"] = appVersion()
|
||||
dict["appVersion"] = appVersion()
|
||||
dict["firstAppBuild"] = appBuild()
|
||||
dict["appBuild"] = appBuild()
|
||||
|
||||
if let loadedDict = dictFromFile(analyticsFileURL) {
|
||||
if let val = loadedDict["firstStart"] as? Double {
|
||||
dict["firstStart"] = val
|
||||
}
|
||||
if let val = loadedDict["lastStart"] as? Double {
|
||||
if update {
|
||||
dict["lastStart"] = now
|
||||
} else {
|
||||
dict["lastStart"] = val
|
||||
}
|
||||
}
|
||||
if let val = loadedDict["starts"] as? Int {
|
||||
if update {
|
||||
dict["starts"] = val + 1
|
||||
} else {
|
||||
dict["starts"] = val
|
||||
}
|
||||
}
|
||||
if let val = loadedDict["uuid"] as? String {
|
||||
dict["uuid"] = val
|
||||
uuid = val
|
||||
}
|
||||
if let val = loadedDict["userName"] as? String {
|
||||
if update && !analyticsUserName.isEmpty {
|
||||
dict["userName"] = analyticsUserName
|
||||
} else {
|
||||
if !val.isEmpty {
|
||||
dict["userName"] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
if let val = loadedDict["firstAppVersion"] as? String {
|
||||
dict["firstAppVersion"] = val
|
||||
}
|
||||
if let val = loadedDict["firstAppBuild"] as? Int {
|
||||
dict["firstAppBuild"] = val
|
||||
}
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
/// Returns the current app version string (like 1.2.5) or empty string on error
|
||||
func appVersion() -> String {
|
||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||
return version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Returns the current app build as integer (like 563, always incrementing) or 0 on error
|
||||
func appBuild() -> Int {
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
if let intVersion = Int(version) {
|
||||
return intVersion
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/// returns optional dict from a json encoded file
|
||||
func dictFromFile(_ url: URL) -> [String: Any]? {
|
||||
do {
|
||||
let fileContent = try String(contentsOfFile: url.path, encoding: .utf8)
|
||||
if let data = fileContent.data(using: .utf8) {
|
||||
return try JSONSerialization.jsonObject(with: data,
|
||||
options: .mutableContainers) as? [String: Any]
|
||||
}
|
||||
} catch {
|
||||
toNSLog("SwiftyBeaver Platform Destination could not read file \(url)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// turns dict into JSON and saves it to file
|
||||
func saveDictToFile(_ dict: [String: Any], url: URL) -> Bool {
|
||||
let jsonString = jsonStringFromDict(dict)
|
||||
|
||||
if let str = jsonString {
|
||||
toNSLog("saving '\(str)' to \(url)")
|
||||
return saveToFile(str, url: url, overwrite: true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Debug Helpers
|
||||
|
||||
/// log String to toNSLog. Used to debug the class logic
|
||||
func toNSLog(_ str: String) {
|
||||
if showNSLog {
|
||||
#if os(Linux)
|
||||
print("SBPlatform: \(str)")
|
||||
#else
|
||||
NSLog("SBPlatform: \(str)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the current thread name
|
||||
class func threadName() -> String {
|
||||
|
||||
#if os(Linux)
|
||||
// on 9/30/2016 not yet implemented in server-side Swift:
|
||||
// > import Foundation
|
||||
// > Thread.isMainThread
|
||||
return ""
|
||||
#else
|
||||
if Thread.isMainThread {
|
||||
return ""
|
||||
} else {
|
||||
let threadName = Thread.current.name
|
||||
if let threadName = threadName, !threadName.isEmpty {
|
||||
return threadName
|
||||
} else {
|
||||
return String(format: "%p", Thread.current)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// SwiftyBeaver.swift
|
||||
// SwiftyBeaver
|
||||
//
|
||||
// Created by Sebastian Kreutzberger (Twitter @skreutzb) on 28.11.15.
|
||||
// Copyright © 2015 Sebastian Kreutzberger
|
||||
// Some rights reserved: http://opensource.org/licenses/MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class SwiftyBeaver {
|
||||
|
||||
/// version string of framework
|
||||
public static let version = "1.9.0" // UPDATE ON RELEASE!
|
||||
/// build number of framework
|
||||
public static let build = 1900 // version 1.6.2 -> 1620, UPDATE ON RELEASE!
|
||||
|
||||
public enum Level: Int {
|
||||
case verbose = 0
|
||||
case debug = 1
|
||||
case info = 2
|
||||
case warning = 3
|
||||
case error = 4
|
||||
}
|
||||
|
||||
// a set of active destinations
|
||||
public private(set) static var destinations = Set<BaseDestination>()
|
||||
|
||||
// MARK: Destination Handling
|
||||
|
||||
/// returns boolean about success
|
||||
@discardableResult
|
||||
open class func addDestination(_ destination: BaseDestination) -> Bool {
|
||||
if destinations.contains(destination) {
|
||||
return false
|
||||
}
|
||||
destinations.insert(destination)
|
||||
return true
|
||||
}
|
||||
|
||||
/// returns boolean about success
|
||||
@discardableResult
|
||||
open class func removeDestination(_ destination: BaseDestination) -> Bool {
|
||||
if destinations.contains(destination) == false {
|
||||
return false
|
||||
}
|
||||
destinations.remove(destination)
|
||||
return true
|
||||
}
|
||||
|
||||
/// if you need to start fresh
|
||||
open class func removeAllDestinations() {
|
||||
destinations.removeAll()
|
||||
}
|
||||
|
||||
/// returns the amount of destinations
|
||||
open class func countDestinations() -> Int {
|
||||
return destinations.count
|
||||
}
|
||||
|
||||
/// returns the current thread name
|
||||
class func threadName() -> String {
|
||||
|
||||
#if os(Linux)
|
||||
// on 9/30/2016 not yet implemented in server-side Swift:
|
||||
// > import Foundation
|
||||
// > Thread.isMainThread
|
||||
return ""
|
||||
#else
|
||||
if Thread.isMainThread {
|
||||
return ""
|
||||
} else {
|
||||
let name = __dispatch_queue_get_label(nil)
|
||||
return String(cString: name, encoding: .utf8) ?? Thread.current.description
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: Levels
|
||||
|
||||
/// log something generally unimportant (lowest priority)
|
||||
open class func verbose(_ message: @autoclosure () -> Any, _
|
||||
file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
custom(level: .verbose, message: message(), file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
custom(level: .verbose, message: message, file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// log something which help during debugging (low priority)
|
||||
open class func debug(_ message: @autoclosure () -> Any, _
|
||||
file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
custom(level: .debug, message: message(), file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
custom(level: .debug, message: message, file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// log something which you are really interested but which is not an issue or error (normal priority)
|
||||
open class func info(_ message: @autoclosure () -> Any, _
|
||||
file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
custom(level: .info, message: message(), file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
custom(level: .info, message: message, file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// log something which may cause big trouble soon (high priority)
|
||||
open class func warning(_ message: @autoclosure () -> Any, _
|
||||
file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
custom(level: .warning, message: message(), file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
custom(level: .warning, message: message, file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// log something which will keep you awake at night (highest priority)
|
||||
open class func error(_ message: @autoclosure () -> Any, _
|
||||
file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
custom(level: .error, message: message(), file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
custom(level: .error, message: message, file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// custom logging to manually adjust values, should just be used by other frameworks
|
||||
public class func custom(level: SwiftyBeaver.Level, message: @autoclosure () -> Any,
|
||||
file: String = #file, function: String = #function, line: Int = #line, context: Any? = nil) {
|
||||
#if swift(>=5)
|
||||
dispatch_send(level: level, message: message(), thread: threadName(),
|
||||
file: file, function: function, line: line, context: context)
|
||||
#else
|
||||
dispatch_send(level: level, message: message, thread: threadName(),
|
||||
file: file, function: function, line: line, context: context)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// internal helper which dispatches send to dedicated queue if minLevel is ok
|
||||
class func dispatch_send(level: SwiftyBeaver.Level, message: @autoclosure () -> Any,
|
||||
thread: String, file: String, function: String, line: Int, context: Any?) {
|
||||
var resolvedMessage: String?
|
||||
for dest in destinations {
|
||||
|
||||
guard let queue = dest.queue else {
|
||||
continue
|
||||
}
|
||||
|
||||
resolvedMessage = resolvedMessage == nil && dest.hasMessageFilters() ? "\(message())" : resolvedMessage
|
||||
if dest.shouldLevelBeLogged(level, path: file, function: function, message: resolvedMessage) {
|
||||
// try to convert msg object to String and put it on queue
|
||||
let msgStr = resolvedMessage == nil ? "\(message())" : resolvedMessage!
|
||||
let f = stripParams(function: function)
|
||||
|
||||
if dest.asynchronously {
|
||||
queue.async {
|
||||
_ = dest.send(level, msg: msgStr, thread: thread, file: file, function: f, line: line, context: context)
|
||||
}
|
||||
} else {
|
||||
queue.sync {
|
||||
_ = dest.send(level, msg: msgStr, thread: thread, file: file, function: f, line: line, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
DEPRECATED & NEEDS COMPLETE REWRITE DUE TO SWIFT 3 AND GENERAL INCORRECT LOGIC
|
||||
Flush all destinations to make sure all logging messages have been written out
|
||||
Returns after all messages flushed or timeout seconds
|
||||
|
||||
- returns: true if all messages flushed, false if timeout or error occurred
|
||||
*/
|
||||
public class func flush(secondTimeout: Int64) -> Bool {
|
||||
|
||||
/*
|
||||
guard let grp = dispatch_group_create() else { return false }
|
||||
for dest in destinations {
|
||||
if let queue = dest.queue {
|
||||
dispatch_group_enter(grp)
|
||||
queue.asynchronously(execute: {
|
||||
dest.flush()
|
||||
grp.leave()
|
||||
})
|
||||
}
|
||||
}
|
||||
let waitUntil = DispatchTime.now(dispatch_time_t(DISPATCH_TIME_NOW), secondTimeout * 1000000000)
|
||||
return dispatch_group_wait(grp, waitUntil) == 0
|
||||
*/
|
||||
return true
|
||||
}
|
||||
|
||||
/// removes the parameters from a function because it looks weird with a single param
|
||||
class func stripParams(function: String) -> String {
|
||||
var f = function
|
||||
if let indexOfBrace = f.find("(") {
|
||||
#if swift(>=4.0)
|
||||
f = String(f[..<indexOfBrace])
|
||||
#else
|
||||
f = f.substring(to: indexOfBrace)
|
||||
#endif
|
||||
}
|
||||
f += "()"
|
||||
return f
|
||||
}
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
# SwiftyMarkdown 1.0
|
||||
|
||||
SwiftyMarkdown converts Markdown files and strings into `NSAttributedString`s using sensible defaults and a Swift-style syntax. It uses dynamic type to set the font size correctly with whatever font you'd like to use.
|
||||
|
||||
- [What's New](#fully-rebuilt-for-2020)
|
||||
- [Installation](#installation)
|
||||
- [How to Use](#how-to-use-swiftymarkdown)
|
||||
- [Screenshot](#screenshot)
|
||||
- [Front Matter](#front-matter)
|
||||
- [Appendix](#appendix)
|
||||
|
||||
## Fully Rebuilt For 2020!
|
||||
|
||||
SwiftyMarkdown now features a more robust and reliable rules-based line processing and character tokenisation engine. It has added support for images stored in the bundle (``), codeblocks, blockquotes, and unordered lists!
|
||||
|
||||
Line-level attributes can now have a paragraph alignment applied to them (e.g. `h2.aligment = .center`), and links can be optionally underlined by setting `underlineLinks` to `true`.
|
||||
|
||||
It also uses the system color `.label` as the default font color on iOS 13 and above for Dark Mode support out of the box.
|
||||
|
||||
Support for all of Apple's platforms has been enabled.
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods:
|
||||
|
||||
`pod 'SwiftyMarkdown'`
|
||||
|
||||
### SPM:
|
||||
|
||||
In Xcode, `File -> Swift Packages -> Add Package Dependency` and add the GitHub URL.
|
||||
|
||||
## How To Use SwiftyMarkdown
|
||||
|
||||
Read Markdown from a text string...
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString()
|
||||
```
|
||||
|
||||
...or from a URL.
|
||||
|
||||
```swift
|
||||
if let url = Bundle.main.url(forResource: "file", withExtension: "md"), md = SwiftyMarkdown(url: url ) {
|
||||
md.attributedString()
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use a different string once SwiftyMarkdown has been initialised, you can now do so like this:
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
md.attributedString(from: "A **SECOND** Markdown string. *Fancy!*")
|
||||
```
|
||||
|
||||
The attributed string can then be assigned to any label or text control that has support for attributed text.
|
||||
|
||||
```swift
|
||||
let md = SwiftyMarkdown(string: "# Heading\nMy *Markdown* string")
|
||||
let label = UILabel()
|
||||
label.attributedText = md.attributedString()
|
||||
```
|
||||
|
||||
## Supported Markdown Features
|
||||
|
||||
*italics* or _italics_
|
||||
**bold** or __bold__
|
||||
~~Linethrough~~Strikethroughs.
|
||||
`code`
|
||||
|
||||
# Header 1
|
||||
|
||||
or
|
||||
|
||||
Header 1
|
||||
====
|
||||
|
||||
## Header 2
|
||||
|
||||
or
|
||||
|
||||
Header 2
|
||||
---
|
||||
|
||||
### Header 3
|
||||
#### Header 4
|
||||
##### Header 5 #####
|
||||
###### Header 6 ######
|
||||
|
||||
Indented code blocks (spaces or tabs)
|
||||
|
||||
[Links](http://voyagetravelapps.com/)
|
||||

|
||||
|
||||
> Blockquotes
|
||||
|
||||
- Bulleted
|
||||
- Lists
|
||||
- Including indented lists
|
||||
- Up to three levels
|
||||
- Neat!
|
||||
|
||||
1. Ordered
|
||||
1. Lists
|
||||
1. Including indented lists
|
||||
- Up to three levels
|
||||
|
||||
|
||||
|
||||
Compound rules also work, for example:
|
||||
|
||||
It recognises **[Bold Links](http://voyagetravelapps.com/)**
|
||||
|
||||
Or [**Bold Links**](http://voyagetravelapps.com/)
|
||||
|
||||
Images will be inserted into the returned `NSAttributedString` as an `NSTextAttachment` (sadly, this will not work on watchOS as `NSTextAttachment` is not available).
|
||||
|
||||
## Customisation
|
||||
|
||||
Set the attributes of every paragraph and character style type using straightforward dot syntax:
|
||||
|
||||
```swift
|
||||
md.body.fontName = "AvenirNextCondensed-Medium"
|
||||
|
||||
md.h1.color = UIColor.redColor()
|
||||
md.h1.fontName = "AvenirNextCondensed-Bold"
|
||||
md.h1.fontSize = 16
|
||||
md.h1.alignmnent = .center
|
||||
|
||||
md.italic.color = UIColor.blueColor()
|
||||
|
||||
md.underlineLinks = true
|
||||
|
||||
md.bullet = "🍏"
|
||||
```
|
||||
|
||||
On iOS, Specified font sizes will be adjusted relative to the the user's dynamic type settings.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
There's an example project included in the repository. Open the `.xcworkspace` file to get started.
|
||||
|
||||
## Front Matter
|
||||
|
||||
SwiftyMarkdown recognises YAML front matter and will populate the `frontMatterAttributes` property with the key-value pairs that it fines.
|
||||
|
||||
## Appendix
|
||||
|
||||
### A) All Customisable Properties
|
||||
|
||||
```swift
|
||||
h1.fontName : String
|
||||
h1.fontSize : CGFloat
|
||||
h1.color : UI/NSColor
|
||||
h1.fontStyle : FontStyle
|
||||
h1.alignment : NSTextAlignment
|
||||
|
||||
h2.fontName : String
|
||||
h2.fontSize : CGFloat
|
||||
h2.color : UI/NSColor
|
||||
h2.fontStyle : FontStyle
|
||||
h2.alignment : NSTextAlignment
|
||||
|
||||
h3.fontName : String
|
||||
h3.fontSize : CGFloat
|
||||
h3.color : UI/NSColor
|
||||
h3.fontStyle : FontStyle
|
||||
h3.alignment : NSTextAlignment
|
||||
|
||||
h4.fontName : String
|
||||
h4.fontSize : CGFloat
|
||||
h4.color : UI/NSColor
|
||||
h4.fontStyle : FontStyle
|
||||
h4.alignment : NSTextAlignment
|
||||
|
||||
h5.fontName : String
|
||||
h5.fontSize : CGFloat
|
||||
h5.color : UI/NSColor
|
||||
h5.fontStyle : FontStyle
|
||||
h5.alignment : NSTextAlignment
|
||||
|
||||
h6.fontName : String
|
||||
h6.fontSize : CGFloat
|
||||
h6.color : UI/NSColor
|
||||
h6.fontStyle : FontStyle
|
||||
h6.alignment : NSTextAlignment
|
||||
|
||||
body.fontName : String
|
||||
body.fontSize : CGFloat
|
||||
body.color : UI/NSColor
|
||||
body.fontStyle : FontStyle
|
||||
body.alignment : NSTextAlignment
|
||||
|
||||
blockquotes.fontName : String
|
||||
blockquotes.fontSize : CGFloat
|
||||
blockquotes.color : UI/NSColor
|
||||
blockquotes.fontStyle : FontStyle
|
||||
blockquotes.alignment : NSTextAlignment
|
||||
|
||||
link.fontName : String
|
||||
link.fontSize : CGFloat
|
||||
link.color : UI/NSColor
|
||||
link.fontStyle : FontStyle
|
||||
|
||||
bold.fontName : String
|
||||
bold.fontSize : CGFloat
|
||||
bold.color : UI/NSColor
|
||||
bold.fontStyle : FontStyle
|
||||
|
||||
italic.fontName : String
|
||||
italic.fontSize : CGFloat
|
||||
italic.color : UI/NSColor
|
||||
italic.fontStyle : FontStyle
|
||||
|
||||
code.fontName : String
|
||||
code.fontSize : CGFloat
|
||||
code.color : UI/NSColor
|
||||
code.fontStyle : FontStyle
|
||||
|
||||
strikethrough.fontName : String
|
||||
strikethrough.fontSize : CGFloat
|
||||
strikethrough.color : UI/NSColor
|
||||
strikethrough.fontStyle : FontStyle
|
||||
|
||||
underlineLinks : Bool
|
||||
|
||||
bullet : String
|
||||
```
|
||||
|
||||
`FontStyle` is an enum with these cases: `normal`, `bold`, `italic`, and `bolditalic` to give you more precise control over how lines and character styles should look.
|
||||
|
||||
If you like a bit of chaos:
|
||||
|
||||
```swift
|
||||
md.bold.fontStyle = .italic
|
||||
md.italic.fontStyle = .bold
|
||||
```
|
||||
|
||||
### B) Advanced Customisation
|
||||
|
||||
SwiftyMarkdown uses a rules-based line processing and customisation engine that is no longer limited to Markdown. Rules are processed in order, from top to bottom. Line processing happens first, then character styles are applied based on the character rules.
|
||||
|
||||
For example, here's how a small subset of Markdown line tags are set up within SwiftyMarkdown:
|
||||
|
||||
```swift
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
case h1
|
||||
case h2
|
||||
case previousH1
|
||||
case codeblock
|
||||
case body
|
||||
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static public var lineRules = [
|
||||
LineRule(token: " ",type : MarkdownLineStyle.codeblock, removeFrom: .leading),
|
||||
LineRule(token: "=",type : MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, default: MarkdownLineStyle.body)
|
||||
```
|
||||
|
||||
Similarly, the character styles all follow rules:
|
||||
|
||||
```swift
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case link, bold, italic, code
|
||||
}
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
```
|
||||
|
||||
#### Rule Subsets
|
||||
|
||||
If you want to only support a small subset of Markdown, it's now easy to do.
|
||||
|
||||
This example would only process strings with `*` and `_` characters, ignoring links, images, code, and all line-level attributes (headings, blockquotes, etc.)
|
||||
```swift
|
||||
SwiftyMarkdown.lineRules = []
|
||||
|
||||
SwiftyMarkdown.characterRules = [
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
```
|
||||
|
||||
#### Custom Rules
|
||||
|
||||
If you wanted to create a rule that applied a style of `Elf` to a range of characters between "The elf will speak now: %Here is my elf speaking%", you could set things up like this:
|
||||
|
||||
```swift
|
||||
enum Characters : CharacterStyling {
|
||||
case elf
|
||||
|
||||
func isEqualTo( _ other : CharacterStyling) -> Bool {
|
||||
if let other = other as? Characters else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
let characterRules = [
|
||||
CharacterRule(openTag: "%", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.elf]], maxTags: 1)
|
||||
]
|
||||
|
||||
let processor = SwiftyTokeniser( with : characterRules )
|
||||
let string = "The elf will speak now: %Here is my elf speaking%"
|
||||
let tokens = processor.process(string)
|
||||
```
|
||||
|
||||
The output is an array of tokens would be equivalent to:
|
||||
|
||||
```swift
|
||||
[
|
||||
Token(type: .string, inputString: "The elf will speak now: ", characterStyles: []),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: []),
|
||||
Token(type: .string, inputString: "Here is my elf speaking", characterStyles: [.elf]),
|
||||
Token(type: .repeatingTag, inputString: "%", characterStyles: [])
|
||||
]
|
||||
```
|
||||
|
||||
### C) SpriteKit Support
|
||||
|
||||
Did you know that `SKLabelNode` supports attributed text? I didn't.
|
||||
|
||||
```swift
|
||||
let smd = SwiftyMarkdown(string: "My Character's **Dialogue**")
|
||||
|
||||
let label = SKLabelNode()
|
||||
label.preferredMaxLayoutWidth = 500
|
||||
label.numberOfLines = 0
|
||||
label.attributedText = smd.attributedString()
|
||||
```
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// String+SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 08/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Some helper functions based on this:
|
||||
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
|
||||
extension StringProtocol {
|
||||
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.lowerBound
|
||||
}
|
||||
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
|
||||
range(of: string, options: options)?.upperBound
|
||||
}
|
||||
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
|
||||
var indices: [Index] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
indices.append(range.lowerBound)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return indices
|
||||
}
|
||||
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
|
||||
var result: [Range<Index>] = []
|
||||
var startIndex = self.startIndex
|
||||
while startIndex < endIndex,
|
||||
let range = self[startIndex...]
|
||||
.range(of: string, options: options) {
|
||||
result.append(range)
|
||||
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
|
||||
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
//
|
||||
// SwiftyLineProcessor.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol LineStyling {
|
||||
var shouldTokeniseLine : Bool { get }
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling?
|
||||
}
|
||||
|
||||
public struct SwiftyLine : CustomStringConvertible {
|
||||
public let line : String
|
||||
public let lineStyle : LineStyling
|
||||
public var description: String {
|
||||
return self.line
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftyLine : Equatable {
|
||||
public static func == ( _ lhs : SwiftyLine, _ rhs : SwiftyLine ) -> Bool {
|
||||
return lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Remove {
|
||||
case leading
|
||||
case trailing
|
||||
case both
|
||||
case entireLine
|
||||
case none
|
||||
}
|
||||
|
||||
public enum ChangeApplication {
|
||||
case current
|
||||
case previous
|
||||
case untilClose
|
||||
}
|
||||
|
||||
public struct FrontMatterRule {
|
||||
let openTag : String
|
||||
let closeTag : String
|
||||
let keyValueSeparator : Character
|
||||
}
|
||||
|
||||
public struct LineRule {
|
||||
let token : String
|
||||
let removeFrom : Remove
|
||||
let type : LineStyling
|
||||
let shouldTrim : Bool
|
||||
let changeAppliesTo : ChangeApplication
|
||||
|
||||
public init(token : String, type : LineStyling, removeFrom : Remove = .leading, shouldTrim : Bool = true, changeAppliesTo : ChangeApplication = .current ) {
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.removeFrom = removeFrom
|
||||
self.shouldTrim = shouldTrim
|
||||
self.changeAppliesTo = changeAppliesTo
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyLineProcessor {
|
||||
|
||||
public var processEmptyStrings : LineStyling?
|
||||
public internal(set) var frontMatterAttributes : [String : String] = [:]
|
||||
|
||||
var closeToken : String? = nil
|
||||
let defaultType : LineStyling
|
||||
|
||||
let lineRules : [LineRule]
|
||||
let frontMatterRules : [FrontMatterRule]
|
||||
|
||||
public init( rules : [LineRule], defaultRule: LineStyling, frontMatterRules : [FrontMatterRule] = []) {
|
||||
self.lineRules = rules
|
||||
self.defaultType = defaultRule
|
||||
self.frontMatterRules = frontMatterRules
|
||||
}
|
||||
|
||||
func findLeadingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
if let range = output.index(output.startIndex, offsetBy: element.token.count, limitedBy: output.endIndex), output[output.startIndex..<range] == element.token {
|
||||
output.removeSubrange(output.startIndex..<range)
|
||||
return output
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func findTrailingLineElement( _ element : LineRule, in string : String ) -> String {
|
||||
var output = string
|
||||
let token = element.token.trimmingCharacters(in: .whitespaces)
|
||||
if let range = output.index(output.endIndex, offsetBy: -(token.count), limitedBy: output.startIndex), output[range..<output.endIndex] == token {
|
||||
output.removeSubrange(range..<output.endIndex)
|
||||
return output
|
||||
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func processLineLevelAttributes( _ text : String ) -> SwiftyLine? {
|
||||
if text.isEmpty, let style = processEmptyStrings {
|
||||
return SwiftyLine(line: "", lineStyle: style)
|
||||
}
|
||||
let previousLines = lineRules.filter({ $0.changeAppliesTo == .previous })
|
||||
|
||||
for element in lineRules {
|
||||
guard element.token.count > 0 else {
|
||||
continue
|
||||
}
|
||||
var output : String = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let unprocessed = output
|
||||
|
||||
if let hasToken = self.closeToken, unprocessed != hasToken {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch element.removeFrom {
|
||||
case .leading:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
case .trailing:
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .both:
|
||||
output = findLeadingLineElement(element, in: output)
|
||||
output = findTrailingLineElement(element, in: output)
|
||||
case .entireLine:
|
||||
let maybeOutput = output.replacingOccurrences(of: element.token, with: "")
|
||||
output = ( maybeOutput.isEmpty ) ? maybeOutput : output
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Only if the output has changed in some way
|
||||
guard unprocessed != output else {
|
||||
continue
|
||||
}
|
||||
if element.changeAppliesTo == .untilClose {
|
||||
self.closeToken = (self.closeToken == nil) ? element.token : nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
output = (element.shouldTrim) ? output.trimmingCharacters(in: .whitespaces) : output
|
||||
return SwiftyLine(line: output, lineStyle: element.type)
|
||||
|
||||
}
|
||||
|
||||
for element in previousLines {
|
||||
let output = (element.shouldTrim) ? text.trimmingCharacters(in: .whitespaces) : text
|
||||
let charSet = CharacterSet(charactersIn: element.token )
|
||||
if output.unicodeScalars.allSatisfy({ charSet.contains($0) }) {
|
||||
return SwiftyLine(line: "", lineStyle: element.type)
|
||||
}
|
||||
}
|
||||
|
||||
return SwiftyLine(line: text.trimmingCharacters(in: .whitespaces), lineStyle: defaultType)
|
||||
}
|
||||
|
||||
func processFrontMatter( _ strings : [String] ) -> [String] {
|
||||
guard let firstString = strings.first?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
return strings
|
||||
}
|
||||
var rulesToApply : FrontMatterRule? = nil
|
||||
for matter in self.frontMatterRules {
|
||||
if firstString == matter.openTag {
|
||||
rulesToApply = matter
|
||||
break
|
||||
}
|
||||
}
|
||||
guard let existentRules = rulesToApply else {
|
||||
return strings
|
||||
}
|
||||
var outputString = strings
|
||||
// Remove the first line, which is the front matter opening tag
|
||||
let _ = outputString.removeFirst()
|
||||
var closeFound = false
|
||||
while !closeFound {
|
||||
let nextString = outputString.removeFirst()
|
||||
if nextString == existentRules.closeTag {
|
||||
closeFound = true
|
||||
continue
|
||||
}
|
||||
var keyValue = nextString.components(separatedBy: "\(existentRules.keyValueSeparator)")
|
||||
if keyValue.count < 2 {
|
||||
continue
|
||||
}
|
||||
let key = keyValue.removeFirst()
|
||||
let value = keyValue.joined()
|
||||
self.frontMatterAttributes[key] = value
|
||||
}
|
||||
while outputString.first?.isEmpty ?? false {
|
||||
outputString.removeFirst()
|
||||
}
|
||||
return outputString
|
||||
}
|
||||
|
||||
public func process( _ string : String ) -> [SwiftyLine] {
|
||||
var foundAttributes : [SwiftyLine] = []
|
||||
|
||||
var lines = string.components(separatedBy: CharacterSet.newlines)
|
||||
lines = self.processFrontMatter(lines)
|
||||
|
||||
for heading in lines {
|
||||
|
||||
if processEmptyStrings == nil && heading.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let input = processLineLevelAttributes(String(heading)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let existentPrevious = input.lineStyle.styleIfFoundStyleAffectsPreviousLine(), foundAttributes.count > 0 {
|
||||
if let idx = foundAttributes.firstIndex(of: foundAttributes.last!) {
|
||||
let updatedPrevious = foundAttributes.last!
|
||||
foundAttributes[idx] = SwiftyLine(line: updatedPrevious.line, lineStyle: existentPrevious)
|
||||
}
|
||||
continue
|
||||
}
|
||||
foundAttributes.append(input)
|
||||
}
|
||||
return foundAttributes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> UIFont {
|
||||
let textStyle : UIFont.TextStyle
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title1
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h2:
|
||||
style = self.h2
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
}
|
||||
case .h3:
|
||||
style = self.h3
|
||||
if #available(iOS 9, *) {
|
||||
textStyle = UIFont.TextStyle.title2
|
||||
} else {
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
}
|
||||
case .h4:
|
||||
style = self.h4
|
||||
textStyle = UIFont.TextStyle.headline
|
||||
case .h5:
|
||||
style = self.h5
|
||||
textStyle = UIFont.TextStyle.subheadline
|
||||
case .h6:
|
||||
style = self.h6
|
||||
textStyle = UIFont.TextStyle.footnote
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
textStyle = UIFont.TextStyle.body
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
textStyle = UIFont.TextStyle.body
|
||||
default:
|
||||
style = self.body
|
||||
textStyle = UIFont.TextStyle.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
case .strikethrough:
|
||||
fontName = strikethrough.fontName ?? fontName
|
||||
fontSize = strikethrough.fontSize
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
var font : UIFont
|
||||
if let existentFontName = fontName {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
let styleDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||
finalSize = styleDescriptor.fontAttributes[.size] as? CGFloat ?? CGFloat(14)
|
||||
}
|
||||
|
||||
if let customFont = UIFont(name: existentFontName, size: finalSize) {
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
|
||||
font = fontMetrics.scaledFont(for: customFont)
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
} else {
|
||||
font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||
}
|
||||
|
||||
if globalItalic, let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
||||
font = UIFont(descriptor: italicDescriptor, size: 0)
|
||||
}
|
||||
if globalBold, let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) {
|
||||
font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> UIColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .yaml:
|
||||
return body.color
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown+macOS.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 17/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func font( for line : SwiftyLine, characterOverride : CharacterStyle? = nil ) -> NSFont {
|
||||
var fontName : String?
|
||||
var fontSize : CGFloat?
|
||||
|
||||
var globalBold = false
|
||||
var globalItalic = false
|
||||
|
||||
let style : FontProperties
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1:
|
||||
style = self.h1
|
||||
case .h2:
|
||||
style = self.h2
|
||||
case .h3:
|
||||
style = self.h3
|
||||
case .h4:
|
||||
style = self.h4
|
||||
case .h5:
|
||||
style = self.h5
|
||||
case .h6:
|
||||
style = self.h6
|
||||
case .codeblock:
|
||||
style = self.code
|
||||
case .blockquote:
|
||||
style = self.blockquotes
|
||||
default:
|
||||
style = self.body
|
||||
}
|
||||
|
||||
fontName = style.fontName
|
||||
fontSize = style.fontSize
|
||||
switch style.fontStyle {
|
||||
case .bold:
|
||||
globalBold = true
|
||||
case .italic:
|
||||
globalItalic = true
|
||||
case .boldItalic:
|
||||
globalItalic = true
|
||||
globalBold = true
|
||||
case .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if fontName == nil {
|
||||
fontName = body.fontName
|
||||
}
|
||||
|
||||
if let characterOverride = characterOverride {
|
||||
switch characterOverride {
|
||||
case .code:
|
||||
fontName = code.fontName ?? fontName
|
||||
fontSize = code.fontSize
|
||||
case .link:
|
||||
fontName = link.fontName ?? fontName
|
||||
fontSize = link.fontSize
|
||||
case .bold:
|
||||
fontName = bold.fontName ?? fontName
|
||||
fontSize = bold.fontSize
|
||||
globalBold = true
|
||||
case .italic:
|
||||
fontName = italic.fontName ?? fontName
|
||||
fontSize = italic.fontSize
|
||||
globalItalic = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fontSize = fontSize == 0.0 ? nil : fontSize
|
||||
let finalSize : CGFloat
|
||||
if let existentFontSize = fontSize {
|
||||
finalSize = existentFontSize
|
||||
} else {
|
||||
finalSize = NSFont.systemFontSize
|
||||
}
|
||||
var font : NSFont
|
||||
if let existentFontName = fontName {
|
||||
if let customFont = NSFont(name: existentFontName, size: finalSize) {
|
||||
font = customFont
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
} else {
|
||||
font = NSFont.systemFont(ofSize: finalSize)
|
||||
}
|
||||
|
||||
if globalItalic {
|
||||
let italicDescriptor = font.fontDescriptor.withSymbolicTraits(.italic)
|
||||
font = NSFont(descriptor: italicDescriptor, size: 0) ?? font
|
||||
}
|
||||
if globalBold {
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.bold)
|
||||
font = NSFont(descriptor: boldDescriptor, size: 0) ?? font
|
||||
}
|
||||
|
||||
return font
|
||||
|
||||
}
|
||||
|
||||
func color( for line : SwiftyLine ) -> NSColor {
|
||||
// What type are we and is there a font name set?
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .h1, .previousH1:
|
||||
return h1.color
|
||||
case .h2, .previousH2:
|
||||
return h2.color
|
||||
case .h3:
|
||||
return h3.color
|
||||
case .h4:
|
||||
return h4.color
|
||||
case .h5:
|
||||
return h5.color
|
||||
case .h6:
|
||||
return h6.color
|
||||
case .body:
|
||||
return body.color
|
||||
case .codeblock:
|
||||
return code.color
|
||||
case .blockquote:
|
||||
return blockquotes.color
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
return body.color
|
||||
case .yaml:
|
||||
return body.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,545 +0,0 @@
|
||||
//
|
||||
// SwiftyMarkdown.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 05/03/2016.
|
||||
// Copyright © 2016 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum CharacterStyle : CharacterStyling {
|
||||
case none
|
||||
case bold
|
||||
case italic
|
||||
case code
|
||||
case link
|
||||
case image
|
||||
case strikethrough
|
||||
|
||||
func isEqualTo(_ other: CharacterStyling) -> Bool {
|
||||
guard let other = other as? CharacterStyle else {
|
||||
return false
|
||||
}
|
||||
return other == self
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkdownLineStyle : LineStyling {
|
||||
var shouldTokeniseLine: Bool {
|
||||
switch self {
|
||||
case .codeblock:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
case yaml
|
||||
case h1
|
||||
case h2
|
||||
case h3
|
||||
case h4
|
||||
case h5
|
||||
case h6
|
||||
case previousH1
|
||||
case previousH2
|
||||
case body
|
||||
case blockquote
|
||||
case codeblock
|
||||
case unorderedList
|
||||
case unorderedListIndentFirstOrder
|
||||
case unorderedListIndentSecondOrder
|
||||
case orderedList
|
||||
case orderedListIndentFirstOrder
|
||||
case orderedListIndentSecondOrder
|
||||
|
||||
|
||||
func styleIfFoundStyleAffectsPreviousLine() -> LineStyling? {
|
||||
switch self {
|
||||
case .previousH1:
|
||||
return MarkdownLineStyle.h1
|
||||
case .previousH2:
|
||||
return MarkdownLineStyle.h2
|
||||
default :
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum FontStyle : Int {
|
||||
case normal
|
||||
case bold
|
||||
case italic
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : NSColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#else
|
||||
@objc public protocol FontProperties {
|
||||
var fontName : String? { get set }
|
||||
var color : UIColor { get set }
|
||||
var fontSize : CGFloat { get set }
|
||||
var fontStyle : FontStyle { get set }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@objc public protocol LineProperties {
|
||||
var alignment : NSTextAlignment { get set }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A class defining the styles that can be applied to the parsed Markdown. The `fontName` property is optional, and if it's not set then the `fontName` property of the Body style will be applied.
|
||||
|
||||
If that is not set, then the system default will be used.
|
||||
*/
|
||||
@objc open class BasicStyles : NSObject, FontProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
}
|
||||
|
||||
@objc open class LineStyles : NSObject, FontProperties, LineProperties {
|
||||
public var fontName : String?
|
||||
#if os(macOS)
|
||||
public var color = NSColor.black
|
||||
#else
|
||||
public var color = UIColor.black
|
||||
#endif
|
||||
public var fontSize : CGFloat = 0.0
|
||||
public var fontStyle : FontStyle = .normal
|
||||
public var alignment: NSTextAlignment = .left
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// A class that takes a [Markdown](https://daringfireball.net/projects/markdown/) string or file and returns an NSAttributedString with the applied styles. Supports Dynamic Type.
|
||||
@objc open class SwiftyMarkdown: NSObject {
|
||||
static public var lineRules = [
|
||||
|
||||
LineRule(token: "=", type: MarkdownLineStyle.previousH1, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "-", type: MarkdownLineStyle.previousH2, removeFrom: .entireLine, changeAppliesTo: .previous),
|
||||
LineRule(token: "\t\t- ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t- ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "- ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: "\t\t* ", type: MarkdownLineStyle.unorderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t* ", type: MarkdownLineStyle.unorderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t\t1. ", type: MarkdownLineStyle.orderedListIndentSecondOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t1. ", type: MarkdownLineStyle.orderedListIndentFirstOrder, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "1. ",type : MarkdownLineStyle.orderedList, removeFrom: .leading),
|
||||
LineRule(token: "* ",type : MarkdownLineStyle.unorderedList, removeFrom: .leading),
|
||||
LineRule(token: " ", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: "\t", type: MarkdownLineStyle.codeblock, removeFrom: .leading, shouldTrim: false),
|
||||
LineRule(token: ">",type : MarkdownLineStyle.blockquote, removeFrom: .leading),
|
||||
LineRule(token: "###### ",type : MarkdownLineStyle.h6, removeFrom: .both),
|
||||
LineRule(token: "##### ",type : MarkdownLineStyle.h5, removeFrom: .both),
|
||||
LineRule(token: "#### ",type : MarkdownLineStyle.h4, removeFrom: .both),
|
||||
LineRule(token: "### ",type : MarkdownLineStyle.h3, removeFrom: .both),
|
||||
LineRule(token: "## ",type : MarkdownLineStyle.h2, removeFrom: .both),
|
||||
LineRule(token: "# ",type : MarkdownLineStyle.h1, removeFrom: .both)
|
||||
]
|
||||
|
||||
static public var characterRules = [
|
||||
CharacterRule(openTag: "", escapeCharacter: "\\", styles: [1 : [CharacterStyle.image]], maxTags: 1),
|
||||
CharacterRule(openTag: "[", intermediateTag: "](", closingTag: ")", escapeCharacter: "\\", styles: [1 : [CharacterStyle.link]], maxTags: 1),
|
||||
CharacterRule(openTag: "`", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.code]], maxTags: 1, cancels: .allRemaining),
|
||||
CharacterRule(openTag: "~", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [2 : [CharacterStyle.strikethrough]], minTags: 2, maxTags: 2),
|
||||
CharacterRule(openTag: "*", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3),
|
||||
CharacterRule(openTag: "_", intermediateTag: nil, closingTag: nil, escapeCharacter: "\\", styles: [1 : [CharacterStyle.italic], 2 : [CharacterStyle.bold], 3 : [CharacterStyle.bold, CharacterStyle.italic]], maxTags: 3)
|
||||
]
|
||||
|
||||
static public var frontMatterRules = [
|
||||
FrontMatterRule(openTag: "---", closeTag: "---", keyValueSeparator: ":")
|
||||
]
|
||||
|
||||
let lineProcessor = SwiftyLineProcessor(rules: SwiftyMarkdown.lineRules, defaultRule: MarkdownLineStyle.body, frontMatterRules: SwiftyMarkdown.frontMatterRules)
|
||||
let tokeniser = SwiftyTokeniser(with: SwiftyMarkdown.characterRules)
|
||||
|
||||
/// The styles to apply to any H1 headers found in the Markdown
|
||||
open var h1 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H2 headers found in the Markdown
|
||||
open var h2 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H3 headers found in the Markdown
|
||||
open var h3 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H4 headers found in the Markdown
|
||||
open var h4 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H5 headers found in the Markdown
|
||||
open var h5 = LineStyles()
|
||||
|
||||
/// The styles to apply to any H6 headers found in the Markdown
|
||||
open var h6 = LineStyles()
|
||||
|
||||
/// The default body styles. These are the base styles and will be used for e.g. headers if no other styles override them.
|
||||
open var body = LineStyles()
|
||||
|
||||
/// The styles to apply to any blockquotes found in the Markdown
|
||||
open var blockquotes = LineStyles()
|
||||
|
||||
/// The styles to apply to any links found in the Markdown
|
||||
open var link = BasicStyles()
|
||||
|
||||
/// The styles to apply to any bold text found in the Markdown
|
||||
open var bold = BasicStyles()
|
||||
|
||||
/// The styles to apply to any italic text found in the Markdown
|
||||
open var italic = BasicStyles()
|
||||
|
||||
/// The styles to apply to any code blocks or inline code text found in the Markdown
|
||||
open var code = BasicStyles()
|
||||
|
||||
open var strikethrough = BasicStyles()
|
||||
|
||||
public var bullet : String = "・"
|
||||
|
||||
public var underlineLinks : Bool = false
|
||||
|
||||
public var frontMatterAttributes : [String : String] {
|
||||
get {
|
||||
return self.lineProcessor.frontMatterAttributes
|
||||
}
|
||||
}
|
||||
|
||||
var currentType : MarkdownLineStyle = .body
|
||||
|
||||
|
||||
var string : String
|
||||
|
||||
let tagList = "!\\_*`[]()"
|
||||
let validMarkdownTags = CharacterSet(charactersIn: "!\\_*`[]()")
|
||||
|
||||
var orderedListCount = 0
|
||||
var orderedListIndentFirstOrderCount = 0
|
||||
var orderedListIndentSecondOrderCount = 0
|
||||
|
||||
|
||||
/**
|
||||
|
||||
- parameter string: A string containing [Markdown](https://daringfireball.net/projects/markdown/) syntax to be converted to an NSAttributedString
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object
|
||||
*/
|
||||
public init(string : String ) {
|
||||
self.string = string
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
/**
|
||||
A failable initializer that takes a URL and attempts to read it as a UTF-8 string
|
||||
|
||||
- parameter url: The location of the file to read
|
||||
|
||||
- returns: An initialized SwiftyMarkdown object, or nil if the string couldn't be read
|
||||
*/
|
||||
public init?(url : URL ) {
|
||||
|
||||
do {
|
||||
self.string = try NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
|
||||
|
||||
} catch {
|
||||
self.string = ""
|
||||
return nil
|
||||
}
|
||||
super.init()
|
||||
self.setup()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
#if os(macOS)
|
||||
self.setFontColorForAllStyles(with: .labelColor)
|
||||
#elseif !os(watchOS)
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
self.setFontColorForAllStyles(with: .label)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
Set font size for all styles
|
||||
|
||||
- parameter size: size of font
|
||||
*/
|
||||
open func setFontSizeForAllStyles(with size: CGFloat) {
|
||||
h1.fontSize = size
|
||||
h2.fontSize = size
|
||||
h3.fontSize = size
|
||||
h4.fontSize = size
|
||||
h5.fontSize = size
|
||||
h6.fontSize = size
|
||||
body.fontSize = size
|
||||
italic.fontSize = size
|
||||
bold.fontSize = size
|
||||
code.fontSize = size
|
||||
link.fontSize = size
|
||||
link.fontSize = size
|
||||
strikethrough.fontSize = size
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
open func setFontColorForAllStyles(with color: NSColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#else
|
||||
open func setFontColorForAllStyles(with color: UIColor) {
|
||||
h1.color = color
|
||||
h2.color = color
|
||||
h3.color = color
|
||||
h4.color = color
|
||||
h5.color = color
|
||||
h6.color = color
|
||||
body.color = color
|
||||
italic.color = color
|
||||
bold.color = color
|
||||
code.color = color
|
||||
link.color = color
|
||||
blockquotes.color = color
|
||||
strikethrough.color = color
|
||||
}
|
||||
#endif
|
||||
|
||||
open func setFontNameForAllStyles(with name: String) {
|
||||
h1.fontName = name
|
||||
h2.fontName = name
|
||||
h3.fontName = name
|
||||
h4.fontName = name
|
||||
h5.fontName = name
|
||||
h6.fontName = name
|
||||
body.fontName = name
|
||||
italic.fontName = name
|
||||
bold.fontName = name
|
||||
code.fontName = name
|
||||
link.fontName = name
|
||||
blockquotes.fontName = name
|
||||
strikethrough.fontName = name
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Generates an NSAttributedString from the string or URL passed at initialisation. Custom fonts or styles are applied to the appropriate elements when this method is called.
|
||||
|
||||
- returns: An NSAttributedString with the styles applied
|
||||
*/
|
||||
open func attributedString(from markdownString : String? = nil) -> NSAttributedString {
|
||||
if let existentMarkdownString = markdownString {
|
||||
self.string = existentMarkdownString
|
||||
}
|
||||
let attributedString = NSMutableAttributedString(string: "")
|
||||
self.lineProcessor.processEmptyStrings = MarkdownLineStyle.body
|
||||
let foundAttributes : [SwiftyLine] = lineProcessor.process(self.string)
|
||||
|
||||
for (idx, line) in foundAttributes.enumerated() {
|
||||
if idx > 0 {
|
||||
attributedString.append(NSAttributedString(string: "\n"))
|
||||
}
|
||||
let finalTokens = self.tokeniser.process(line.line)
|
||||
attributedString.append(attributedStringFor(tokens: finalTokens, in: line))
|
||||
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SwiftyMarkdown {
|
||||
|
||||
func attributedStringFor( tokens : [Token], in line : SwiftyLine ) -> NSAttributedString {
|
||||
|
||||
var finalTokens = tokens
|
||||
let finalAttributedString = NSMutableAttributedString()
|
||||
var attributes : [NSAttributedString.Key : AnyObject] = [:]
|
||||
|
||||
guard let markdownLineStyle = line.lineStyle as? MarkdownLineStyle else {
|
||||
preconditionFailure("The passed line style is not a valid Markdown Line Style")
|
||||
}
|
||||
|
||||
var listItem = self.bullet
|
||||
switch markdownLineStyle {
|
||||
case .orderedList:
|
||||
self.orderedListCount += 1
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
listItem = "\(self.orderedListCount)."
|
||||
case .orderedListIndentFirstOrder, .unorderedListIndentFirstOrder:
|
||||
self.orderedListIndentFirstOrderCount += 1
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
if markdownLineStyle == .orderedListIndentFirstOrder {
|
||||
listItem = "\(self.orderedListIndentFirstOrderCount)."
|
||||
}
|
||||
|
||||
case .orderedListIndentSecondOrder, .unorderedListIndentSecondOrder:
|
||||
self.orderedListIndentSecondOrderCount += 1
|
||||
if markdownLineStyle == .orderedListIndentSecondOrder {
|
||||
listItem = "\(self.orderedListIndentSecondOrderCount)."
|
||||
}
|
||||
|
||||
default:
|
||||
self.orderedListCount = 0
|
||||
self.orderedListIndentFirstOrderCount = 0
|
||||
self.orderedListIndentSecondOrderCount = 0
|
||||
}
|
||||
|
||||
let lineProperties : LineProperties
|
||||
switch markdownLineStyle {
|
||||
case .h1:
|
||||
lineProperties = self.h1
|
||||
case .h2:
|
||||
lineProperties = self.h2
|
||||
case .h3:
|
||||
lineProperties = self.h3
|
||||
case .h4:
|
||||
lineProperties = self.h4
|
||||
case .h5:
|
||||
lineProperties = self.h5
|
||||
case .h6:
|
||||
lineProperties = self.h6
|
||||
case .codeblock:
|
||||
lineProperties = body
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .blockquote:
|
||||
lineProperties = self.blockquotes
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.firstLineHeadIndent = 20.0
|
||||
paragraphStyle.headIndent = 20.0
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
case .unorderedList, .unorderedListIndentFirstOrder, .unorderedListIndentSecondOrder, .orderedList, .orderedListIndentFirstOrder, .orderedListIndentSecondOrder:
|
||||
|
||||
let interval : CGFloat = 30
|
||||
var addition = interval
|
||||
var indent = ""
|
||||
switch line.lineStyle as! MarkdownLineStyle {
|
||||
case .unorderedListIndentFirstOrder, .orderedListIndentFirstOrder:
|
||||
addition = interval * 2
|
||||
indent = "\t"
|
||||
case .unorderedListIndentSecondOrder, .orderedListIndentSecondOrder:
|
||||
addition = interval * 3
|
||||
indent = "\t\t"
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
lineProperties = body
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: interval, options: [:]), NSTextTab(textAlignment: .left, location: interval, options: [:])]
|
||||
paragraphStyle.defaultTabInterval = interval
|
||||
paragraphStyle.headIndent = addition
|
||||
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
finalTokens.insert(Token(type: .string, inputString: "\(indent)\(listItem)\t"), at: 0)
|
||||
|
||||
case .yaml:
|
||||
lineProperties = body
|
||||
case .previousH1:
|
||||
lineProperties = body
|
||||
case .previousH2:
|
||||
lineProperties = body
|
||||
case .body:
|
||||
lineProperties = body
|
||||
}
|
||||
|
||||
if lineProperties.alignment != .left {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = lineProperties.alignment
|
||||
attributes[.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
|
||||
|
||||
for token in finalTokens {
|
||||
attributes[.font] = self.font(for: line)
|
||||
attributes[.link] = nil
|
||||
attributes[.strikethroughStyle] = nil
|
||||
attributes[.foregroundColor] = self.color(for: line)
|
||||
guard let styles = token.characterStyles as? [CharacterStyle] else {
|
||||
continue
|
||||
}
|
||||
if styles.contains(.italic) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .italic)
|
||||
attributes[.foregroundColor] = self.italic.color
|
||||
}
|
||||
if styles.contains(.bold) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .bold)
|
||||
attributes[.foregroundColor] = self.bold.color
|
||||
}
|
||||
|
||||
if styles.contains(.link), let url = token.metadataString {
|
||||
attributes[.foregroundColor] = self.link.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .link)
|
||||
attributes[.link] = url as AnyObject
|
||||
|
||||
if underlineLinks {
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
if styles.contains(.strikethrough) {
|
||||
attributes[.font] = self.font(for: line, characterOverride: .strikethrough)
|
||||
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue as AnyObject
|
||||
attributes[.foregroundColor] = self.strikethrough.color
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
if styles.contains(.image), let imageName = token.metadataString {
|
||||
#if !os(macOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = UIImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#elseif !os(watchOS)
|
||||
let image1Attachment = NSTextAttachment()
|
||||
image1Attachment.image = NSImage(named: imageName)
|
||||
let str = NSAttributedString(attachment: image1Attachment)
|
||||
finalAttributedString.append(str)
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
#endif
|
||||
|
||||
if styles.contains(.code) {
|
||||
attributes[.foregroundColor] = self.code.color
|
||||
attributes[.font] = self.font(for: line, characterOverride: .code)
|
||||
} else {
|
||||
// Switch back to previous font
|
||||
}
|
||||
let str = NSAttributedString(string: token.outputString, attributes: attributes)
|
||||
finalAttributedString.append(str)
|
||||
}
|
||||
|
||||
return finalAttributedString
|
||||
}
|
||||
}
|
||||
@@ -1,821 +0,0 @@
|
||||
//
|
||||
// SwiftyTokeniser.swift
|
||||
// SwiftyMarkdown
|
||||
//
|
||||
// Created by Simon Fairbairn on 16/12/2019.
|
||||
// Copyright © 2019 Voyage Travel Apps. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension OSLog {
|
||||
private static var subsystem = "SwiftyTokeniser"
|
||||
static let tokenising = OSLog(subsystem: subsystem, category: "Tokenising")
|
||||
static let styling = OSLog(subsystem: subsystem, category: "Styling")
|
||||
}
|
||||
|
||||
// Tag definition
|
||||
public protocol CharacterStyling {
|
||||
func isEqualTo( _ other : CharacterStyling ) -> Bool
|
||||
}
|
||||
|
||||
public enum SpaceAllowed {
|
||||
case no
|
||||
case bothSides
|
||||
case oneSide
|
||||
case leadingSide
|
||||
case trailingSide
|
||||
}
|
||||
|
||||
public enum Cancel {
|
||||
case none
|
||||
case allRemaining
|
||||
case currentSet
|
||||
}
|
||||
|
||||
public struct CharacterRule : CustomStringConvertible {
|
||||
public let openTag : String
|
||||
public let intermediateTag : String?
|
||||
public let closingTag : String?
|
||||
public let escapeCharacter : Character?
|
||||
public let styles : [Int : [CharacterStyling]]
|
||||
public var minTags : Int = 1
|
||||
public var maxTags : Int = 1
|
||||
public var spacesAllowed : SpaceAllowed = .oneSide
|
||||
public var cancels : Cancel = .none
|
||||
|
||||
public var description: String {
|
||||
return "Character Rule with Open tag: \(self.openTag) and current styles : \(self.styles) "
|
||||
}
|
||||
|
||||
public init(openTag: String, intermediateTag: String? = nil, closingTag: String? = nil, escapeCharacter: Character? = nil, styles: [Int : [CharacterStyling]] = [:], minTags : Int = 1, maxTags : Int = 1, cancels : Cancel = .none) {
|
||||
self.openTag = openTag
|
||||
self.intermediateTag = intermediateTag
|
||||
self.closingTag = closingTag
|
||||
self.escapeCharacter = escapeCharacter
|
||||
self.styles = styles
|
||||
self.minTags = minTags
|
||||
self.maxTags = maxTags
|
||||
self.cancels = cancels
|
||||
}
|
||||
}
|
||||
|
||||
// Token definition
|
||||
public enum TokenType {
|
||||
case repeatingTag
|
||||
case openTag
|
||||
case intermediateTag
|
||||
case closeTag
|
||||
case string
|
||||
case escape
|
||||
case replacement
|
||||
}
|
||||
|
||||
|
||||
|
||||
public struct Token {
|
||||
public let id = UUID().uuidString
|
||||
public let type : TokenType
|
||||
public let inputString : String
|
||||
public fileprivate(set) var metadataString : String? = nil
|
||||
public fileprivate(set) var characterStyles : [CharacterStyling] = []
|
||||
public fileprivate(set) var count : Int = 0
|
||||
public fileprivate(set) var shouldSkip : Bool = false
|
||||
public fileprivate(set) var tokenIndex : Int = -1
|
||||
public fileprivate(set) var isProcessed : Bool = false
|
||||
public fileprivate(set) var isMetadata : Bool = false
|
||||
public var outputString : String {
|
||||
get {
|
||||
switch self.type {
|
||||
case .repeatingTag:
|
||||
if count <= 0 {
|
||||
return ""
|
||||
} else {
|
||||
let range = inputString.startIndex..<inputString.index(inputString.startIndex, offsetBy: self.count)
|
||||
return String(inputString[range])
|
||||
}
|
||||
case .openTag, .closeTag, .intermediateTag:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .escape, .string:
|
||||
return (self.isProcessed || self.isMetadata) ? "" : inputString
|
||||
case .replacement:
|
||||
return self.inputString
|
||||
}
|
||||
}
|
||||
}
|
||||
public init( type : TokenType, inputString : String, characterStyles : [CharacterStyling] = []) {
|
||||
self.type = type
|
||||
self.inputString = inputString
|
||||
self.characterStyles = characterStyles
|
||||
}
|
||||
|
||||
func newToken( fromSubstring string: String, isReplacement : Bool) -> Token {
|
||||
var newToken = Token(type: (isReplacement) ? .replacement : .string , inputString: string, characterStyles: self.characterStyles)
|
||||
newToken.metadataString = self.metadataString
|
||||
newToken.isMetadata = self.isMetadata
|
||||
newToken.isProcessed = self.isProcessed
|
||||
return newToken
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Iterator.Element == Token {
|
||||
var oslogDisplay: String {
|
||||
return "[\"\(self.map( { ($0.outputString.isEmpty) ? "\($0.type): \($0.inputString)" : $0.outputString }).joined(separator: "\", \""))\"]"
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftyTokeniser {
|
||||
let rules : [CharacterRule]
|
||||
var replacements : [String : [Token]] = [:]
|
||||
|
||||
var enableLog = (ProcessInfo.processInfo.environment["SwiftyTokeniserLogging"] != nil)
|
||||
|
||||
public init( with rules : [CharacterRule] ) {
|
||||
self.rules = rules
|
||||
}
|
||||
|
||||
|
||||
/// This goes through every CharacterRule in order and applies it to the input string, tokenising the string
|
||||
/// if there are any matches.
|
||||
///
|
||||
/// The for loop in the while loop (yeah, I know) is there to separate strings from within tags to
|
||||
/// those outside them.
|
||||
///
|
||||
/// e.g. "A string with a \[link\]\(url\) tag" would have the "link" text tokenised separately.
|
||||
///
|
||||
/// This is to prevent situations like **\[link**\](url) from returing a bold string.
|
||||
///
|
||||
/// - Parameter inputString: A string to have the CharacterRules in `self.rules` applied to
|
||||
public func process( _ inputString : String ) -> [Token] {
|
||||
guard rules.count > 0 else {
|
||||
return [Token(type: .string, inputString: inputString)]
|
||||
}
|
||||
|
||||
var currentTokens : [Token] = []
|
||||
var mutableRules = self.rules
|
||||
|
||||
|
||||
|
||||
while !mutableRules.isEmpty {
|
||||
let nextRule = mutableRules.removeFirst()
|
||||
|
||||
if enableLog {
|
||||
os_log("------------------------------", log: .tokenising, type: .info)
|
||||
os_log("RULE: %@", log: OSLog.tokenising, type:.info , nextRule.description)
|
||||
}
|
||||
|
||||
if currentTokens.isEmpty {
|
||||
// This means it's the first time through
|
||||
currentTokens = self.applyStyles(to: self.scan(inputString, with: nextRule), usingRule: nextRule)
|
||||
continue
|
||||
}
|
||||
|
||||
var outerStringTokens : [Token] = []
|
||||
var innerStringTokens : [Token] = []
|
||||
var isOuter = true
|
||||
for idx in 0..<currentTokens.count {
|
||||
let nextToken = currentTokens[idx]
|
||||
if nextToken.type == .openTag && nextToken.isProcessed {
|
||||
isOuter = false
|
||||
}
|
||||
if nextToken.type == .closeTag {
|
||||
let ref = UUID().uuidString
|
||||
outerStringTokens.append(Token(type: .replacement, inputString: ref))
|
||||
innerStringTokens.append(nextToken)
|
||||
self.replacements[ref] = self.handleReplacementTokens(innerStringTokens, with: nextRule)
|
||||
innerStringTokens.removeAll()
|
||||
isOuter = true
|
||||
continue
|
||||
}
|
||||
(isOuter) ? outerStringTokens.append(nextToken) : innerStringTokens.append(nextToken)
|
||||
}
|
||||
|
||||
currentTokens = self.handleReplacementTokens(outerStringTokens, with: nextRule)
|
||||
|
||||
var finalTokens : [Token] = []
|
||||
for token in currentTokens {
|
||||
guard token.type == .replacement else {
|
||||
finalTokens.append(token)
|
||||
continue
|
||||
}
|
||||
if let hasReplacement = self.replacements[token.inputString] {
|
||||
if enableLog {
|
||||
os_log("Found replacement for %@", log: .tokenising, type: .info, token.inputString)
|
||||
}
|
||||
|
||||
for var repToken in hasReplacement {
|
||||
guard repToken.type == .string else {
|
||||
finalTokens.append(repToken)
|
||||
continue
|
||||
}
|
||||
for style in token.characterStyles {
|
||||
if !repToken.characterStyles.contains(where: { $0.isEqualTo(style)}) {
|
||||
repToken.characterStyles.append(contentsOf: token.characterStyles)
|
||||
}
|
||||
}
|
||||
|
||||
finalTokens.append(repToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
currentTokens = finalTokens
|
||||
|
||||
|
||||
// Each string could have additional tokens within it, so they have to be scanned as well with the current rule.
|
||||
// The one string token might then be exploded into multiple more tokens
|
||||
}
|
||||
|
||||
if enableLog {
|
||||
os_log("=====RULE PROCESSING COMPLETE=====", log: .tokenising, type: .info)
|
||||
os_log("==================================", log: .tokenising, type: .info)
|
||||
}
|
||||
|
||||
return currentTokens
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// In order to reinsert the original replacements into the new string token, the replacements
|
||||
/// need to be searched for in the incoming string one by one.
|
||||
///
|
||||
/// Using the `newToken(fromSubstring:isReplacement:)` function ensures that any metadata and character styles
|
||||
/// are passed over into the newly created tokens.
|
||||
///
|
||||
/// E.g. A string token that has an `outputString` of "This string AAAAA-BBBBB-CCCCC replacements", with
|
||||
/// a characterStyle of `bold` for the entire string, needs to be separated into the following tokens:
|
||||
///
|
||||
/// - `string`: "This string "
|
||||
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " replacements"
|
||||
///
|
||||
/// Each of these need to have a character style of `bold`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - replacements: An array of `replacement` tokens
|
||||
/// - token: The new `string` token that may contain replacement IDs contained in the `replacements` array
|
||||
func reinsertReplacements(_ replacements : [Token], from stringToken : Token ) -> [Token] {
|
||||
guard !stringToken.outputString.isEmpty && !replacements.isEmpty else {
|
||||
return [stringToken]
|
||||
}
|
||||
var outputTokens : [Token] = []
|
||||
let scanner = Scanner(string: stringToken.outputString)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
|
||||
// Remove any replacements that don't appear in the incoming string
|
||||
var repTokens = replacements.filter({ stringToken.outputString.contains($0.inputString) })
|
||||
|
||||
var testString = "\n"
|
||||
while !scanner.isAtEnd {
|
||||
var outputString : String = ""
|
||||
if repTokens.count > 0 {
|
||||
testString = repTokens.removeFirst().inputString
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
if let nextString = scanner.scanUpToString(testString) {
|
||||
outputString = nextString
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
|
||||
if let outputToken = scanner.scanString(testString) {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else if let outputToken = scanner.scanString(testString) {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else {
|
||||
var oldString : NSString? = nil
|
||||
var tokenString : NSString? = nil
|
||||
scanner.scanUpTo(testString, into: &oldString)
|
||||
if let nextString = oldString {
|
||||
outputString = nextString as String
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputString, isReplacement: false))
|
||||
scanner.scanString(testString, into: &tokenString)
|
||||
if let outputToken = tokenString as String? {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
} else {
|
||||
scanner.scanString(testString, into: &tokenString)
|
||||
if let outputToken = tokenString as String? {
|
||||
outputTokens.append(stringToken.newToken(fromSubstring: outputToken, isReplacement: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputTokens
|
||||
}
|
||||
|
||||
|
||||
/// This function is necessary because a previously tokenised string might have
|
||||
///
|
||||
/// Consider a previously tokenised string, where AAAAA-BBBBB-CCCCC represents a replaced \[link\](url) instance.
|
||||
///
|
||||
/// The incoming tokens will look like this:
|
||||
///
|
||||
/// - `string`: "A \*\*Bold"
|
||||
/// - `replacement` : "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " with a trailing string**"
|
||||
///
|
||||
/// However, because the scanner can only tokenise individual strings, passing in the string values
|
||||
/// of these tokens individually and applying the styles will not correctly detect the starting and
|
||||
/// ending `repeatingTag` instances. (e.g. the scanner will see "A \*\*Bold", and then "AAAAA-BBBBB-CCCCC",
|
||||
/// and finally " with a trailing string\*\*")
|
||||
///
|
||||
/// The strings need to be combined, so that they form a single string:
|
||||
/// A \*\*Bold AAAAA-BBBBB-CCCCC with a trailing string\*\*.
|
||||
/// This string is then parsed and tokenised so that it looks like this:
|
||||
///
|
||||
/// - `string`: "A "
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
/// - `string`: "Bold AAAAA-BBBBB-CCCCC with a trailing string"
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
///
|
||||
/// Finally, the replacements from the original incoming token array are searched for and pulled out
|
||||
/// of this new string, so the final result looks like this:
|
||||
///
|
||||
/// - `string`: "A "
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
/// - `string`: "Bold "
|
||||
/// - `replacement`: "AAAAA-BBBBB-CCCCC"
|
||||
/// - `string`: " with a trailing string"
|
||||
/// - `repeatingTag`: "\*\*"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tokens: The tokens to be combined, scanned, re-tokenised, and merged
|
||||
/// - rule: The character rule currently being applied
|
||||
func scanReplacementTokens( _ tokens : [Token], with rule : CharacterRule ) -> [Token] {
|
||||
guard tokens.count > 0 else {
|
||||
return []
|
||||
}
|
||||
|
||||
let combinedString = tokens.map({ $0.outputString }).joined()
|
||||
|
||||
let nextTokens = self.scan(combinedString, with: rule)
|
||||
var replacedTokens = self.applyStyles(to: nextTokens, usingRule: rule)
|
||||
|
||||
/// It's necessary here to check to see if the first token (which will always represent the styles
|
||||
/// to be applied from previous scans) has any existing metadata or character styles and apply them
|
||||
/// to *all* the string and replacement tokens found by the new scan.
|
||||
for idx in 0..<replacedTokens.count {
|
||||
guard replacedTokens[idx].type == .string || replacedTokens[idx].type == .replacement else {
|
||||
continue
|
||||
}
|
||||
if tokens.first!.metadataString != nil && replacedTokens[idx].metadataString == nil {
|
||||
replacedTokens[idx].metadataString = tokens.first!.metadataString
|
||||
}
|
||||
replacedTokens[idx].characterStyles.append(contentsOf: tokens.first!.characterStyles)
|
||||
}
|
||||
|
||||
// Swap the original replacement tokens back in
|
||||
let replacements = tokens.filter({ $0.type == .replacement })
|
||||
var outputTokens : [Token] = []
|
||||
for token in replacedTokens {
|
||||
guard token.type == .string else {
|
||||
outputTokens.append(token)
|
||||
continue
|
||||
}
|
||||
outputTokens.append(contentsOf: self.reinsertReplacements(replacements, from: token))
|
||||
}
|
||||
|
||||
return outputTokens
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// This function ensures that only concurrent `string` and `replacement` tokens are processed together.
|
||||
///
|
||||
/// i.e. If there is an existing `repeatingTag` token between two strings, then those strings will be
|
||||
/// processed individually. This prevents incorrect parsing of strings like "\*\*\_Should only be bold\*\*\_"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - incomingTokens: A group of tokens whose string tokens and replacement tokens should be combined and re-tokenised
|
||||
/// - rule: The current rule being processed
|
||||
func handleReplacementTokens( _ incomingTokens : [Token], with rule : CharacterRule) -> [Token] {
|
||||
|
||||
// Only combine string and replacements that are next to each other.
|
||||
var newTokenSet : [Token] = []
|
||||
var currentTokenSet : [Token] = []
|
||||
for i in 0..<incomingTokens.count {
|
||||
guard incomingTokens[i].type == .string || incomingTokens[i].type == .replacement else {
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
newTokenSet.append(incomingTokens[i])
|
||||
currentTokenSet.removeAll()
|
||||
continue
|
||||
}
|
||||
guard !incomingTokens[i].isProcessed && !incomingTokens[i].isMetadata && !incomingTokens[i].shouldSkip else {
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
newTokenSet.append(incomingTokens[i])
|
||||
currentTokenSet.removeAll()
|
||||
continue
|
||||
}
|
||||
currentTokenSet.append(incomingTokens[i])
|
||||
}
|
||||
newTokenSet.append(contentsOf: self.scanReplacementTokens(currentTokenSet, with: rule))
|
||||
|
||||
return newTokenSet
|
||||
}
|
||||
|
||||
|
||||
func handleClosingTagFromOpenTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule ) {
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
return
|
||||
}
|
||||
guard let closeTokenIdx = tokens.firstIndex(where: { $0.type == .closeTag && !$0.isProcessed }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var metadataIndex = index
|
||||
// If there's an intermediate tag, get the index of that
|
||||
if rule.intermediateTag != nil {
|
||||
guard let nextTokenIdx = tokens.firstIndex(where: { $0.type == .intermediateTag && !$0.isProcessed }) else {
|
||||
return
|
||||
}
|
||||
metadataIndex = nextTokenIdx
|
||||
let styles : [CharacterStyling] = rule.styles[1] ?? []
|
||||
for i in index..<nextTokenIdx {
|
||||
for style in styles {
|
||||
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataString : String = ""
|
||||
for i in metadataIndex..<closeTokenIdx {
|
||||
if tokens[i].type == .string {
|
||||
metadataString.append(tokens[i].outputString)
|
||||
tokens[i].isMetadata = true
|
||||
}
|
||||
}
|
||||
|
||||
for i in index..<metadataIndex {
|
||||
if tokens[i].type == .string {
|
||||
tokens[i].metadataString = metadataString
|
||||
}
|
||||
}
|
||||
|
||||
tokens[closeTokenIdx].isProcessed = true
|
||||
tokens[metadataIndex].isProcessed = true
|
||||
tokens[index].isProcessed = true
|
||||
}
|
||||
|
||||
|
||||
/// This is here to manage how opening tags are matched with closing tags when they're all the same
|
||||
/// character.
|
||||
///
|
||||
/// Of course, because Markdown is about as loose as a spec can be while still being considered any
|
||||
/// kind of spec, the number of times this character repeats causes different effects. Then there
|
||||
/// is the ill-defined way it should work if the number of opening and closing tags are different.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - index: The index of the current token in the loop
|
||||
/// - tokens: An inout variable of the loop tokens of interest
|
||||
/// - rule: The character rule being applied
|
||||
func handleClosingTagFromRepeatingTag(withIndex index : Int, in tokens: inout [Token], following rule : CharacterRule) {
|
||||
let theToken = tokens[index]
|
||||
|
||||
if enableLog {
|
||||
os_log("Found repeating tag with tag count: %i, tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString, rule.openTag )
|
||||
}
|
||||
|
||||
guard theToken.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
let startIdx = index
|
||||
var endIdx : Int? = nil
|
||||
|
||||
let maxCount = (theToken.count > rule.maxTags) ? rule.maxTags : theToken.count
|
||||
// Try to find exact match first
|
||||
if let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count == theToken.count && $0.id != theToken.id && !$0.isProcessed }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
|
||||
if endIdx == nil, let nextTokenIdx = tokens.firstIndex(where: { $0.inputString.first == theToken.inputString.first && $0.type == theToken.type && $0.count >= 1 && $0.id != theToken.id && !$0.isProcessed }) {
|
||||
endIdx = nextTokenIdx
|
||||
}
|
||||
guard let existentEnd = endIdx else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let styles : [CharacterStyling] = rule.styles[maxCount] ?? []
|
||||
for i in startIdx..<existentEnd {
|
||||
for style in styles {
|
||||
if !tokens[i].characterStyles.contains(where: { $0.isEqualTo(style )}) {
|
||||
tokens[i].characterStyles.append(style)
|
||||
}
|
||||
}
|
||||
if rule.cancels == .allRemaining {
|
||||
tokens[i].shouldSkip = true
|
||||
}
|
||||
}
|
||||
|
||||
let maxEnd = (tokens[existentEnd].count > rule.maxTags) ? rule.maxTags : tokens[existentEnd].count
|
||||
tokens[index].count = theToken.count - maxEnd
|
||||
tokens[existentEnd].count = tokens[existentEnd].count - maxEnd
|
||||
if maxEnd < rule.maxTags {
|
||||
self.handleClosingTagFromRepeatingTag(withIndex: index, in: &tokens, following: rule)
|
||||
} else {
|
||||
tokens[existentEnd].isProcessed = true
|
||||
tokens[index].isProcessed = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func applyStyles( to tokens : [Token], usingRule rule : CharacterRule ) -> [Token] {
|
||||
var mutableTokens : [Token] = tokens
|
||||
|
||||
if enableLog {
|
||||
os_log("Applying styles to tokens: %@", log: .tokenising, type: .info, tokens.oslogDisplay )
|
||||
}
|
||||
for idx in 0..<mutableTokens.count {
|
||||
let token = mutableTokens[idx]
|
||||
switch token.type {
|
||||
case .escape:
|
||||
if enableLog {
|
||||
os_log("Found escape: %@", log: .tokenising, type: .info, token.inputString )
|
||||
}
|
||||
case .repeatingTag:
|
||||
self.handleClosingTagFromRepeatingTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
case .openTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
if enableLog {
|
||||
os_log("Found repeating tag with tags: %@, current rule open tag: %@", log: .tokenising, type: .info, theToken.inputString, rule.openTag )
|
||||
}
|
||||
|
||||
guard rule.closingTag != nil else {
|
||||
|
||||
// If there's an intermediate tag, get the index of that
|
||||
|
||||
// Get the index of the closing tag
|
||||
|
||||
continue
|
||||
}
|
||||
self.handleClosingTagFromOpenTag(withIndex: idx, in: &mutableTokens, following: rule)
|
||||
|
||||
|
||||
case .intermediateTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
if enableLog {
|
||||
os_log("Found intermediate tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
|
||||
}
|
||||
|
||||
case .closeTag:
|
||||
let theToken = mutableTokens[idx]
|
||||
if enableLog {
|
||||
os_log("Found close tag with tag count: %i, tags: %@", log: .tokenising, type: .info, theToken.count, theToken.inputString )
|
||||
}
|
||||
|
||||
case .string:
|
||||
let theToken = mutableTokens[idx]
|
||||
if enableLog {
|
||||
if theToken.isMetadata {
|
||||
os_log("Found Metadata: %@", log: .tokenising, type: .info, theToken.inputString )
|
||||
} else {
|
||||
os_log("Found String: %@", log: .tokenising, type: .info, theToken.inputString )
|
||||
}
|
||||
if let hasMetadata = theToken.metadataString {
|
||||
os_log("...with metadata: %@", log: .tokenising, type: .info, hasMetadata )
|
||||
}
|
||||
}
|
||||
|
||||
case .replacement:
|
||||
if enableLog {
|
||||
os_log("Found replacement with ID: %@", log: .tokenising, type: .info, mutableTokens[idx].inputString )
|
||||
}
|
||||
}
|
||||
}
|
||||
return mutableTokens
|
||||
}
|
||||
|
||||
|
||||
func scan( _ string : String, with rule : CharacterRule) -> [Token] {
|
||||
let scanner = Scanner(string: string)
|
||||
scanner.charactersToBeSkipped = nil
|
||||
var tokens : [Token] = []
|
||||
var set = CharacterSet(charactersIn: "\(rule.openTag)\(rule.intermediateTag ?? "")\(rule.closingTag ?? "")")
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
set.insert(charactersIn: String(existentEscape))
|
||||
}
|
||||
|
||||
var openTagFound = false
|
||||
var openingString = ""
|
||||
while !scanner.isAtEnd {
|
||||
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
if let start = scanner.scanUpToCharacters(from: set) {
|
||||
openingString.append(start)
|
||||
}
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanUpToCharacters(from: set, into: &string)
|
||||
if let existentString = string as String? {
|
||||
openingString.append(existentString)
|
||||
}
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
let lastChar : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
lastChar = ( scanner.currentIndex > string.startIndex ) ? String(string[string.index(before: scanner.currentIndex)..<scanner.currentIndex]) : nil
|
||||
} else {
|
||||
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
|
||||
lastChar = ( scanLocation > string.startIndex ) ? String(string[string.index(before: scanLocation)..<scanLocation]) : nil
|
||||
} else {
|
||||
lastChar = nil
|
||||
}
|
||||
|
||||
}
|
||||
let maybeFoundChars : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0, tvOS 13.0, *) {
|
||||
maybeFoundChars = scanner.scanCharacters(from: set )
|
||||
} else {
|
||||
var string : NSString?
|
||||
scanner.scanCharacters(from: set, into: &string)
|
||||
maybeFoundChars = string as String?
|
||||
}
|
||||
|
||||
let nextChar : String?
|
||||
if #available(iOS 13.0, OSX 10.15, watchOS 6.0,tvOS 13.0, *) {
|
||||
nextChar = (scanner.currentIndex != string.endIndex) ? String(string[scanner.currentIndex]) : nil
|
||||
} else {
|
||||
if let scanLocation = string.index(string.startIndex, offsetBy: scanner.scanLocation, limitedBy: string.endIndex) {
|
||||
nextChar = (scanLocation != string.endIndex) ? String(string[scanLocation]) : nil
|
||||
} else {
|
||||
nextChar = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
guard let foundChars = maybeFoundChars else {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if foundChars == rule.openTag && foundChars.count < rule.minTags {
|
||||
openingString.append(foundChars)
|
||||
continue
|
||||
}
|
||||
|
||||
if !validateSpacing(nextCharacter: nextChar, previousCharacter: lastChar, with: rule) {
|
||||
let escapeString = String("\(rule.escapeCharacter ?? Character(""))")
|
||||
var escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(rule.openTag)", with: rule.openTag)
|
||||
if let hasIntermediateTag = rule.intermediateTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(hasIntermediateTag)", with: hasIntermediateTag)
|
||||
}
|
||||
if let existentClosingTag = rule.closingTag {
|
||||
escaped = foundChars.replacingOccurrences(of: "\(escapeString)\(existentClosingTag)", with: existentClosingTag)
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
var cumulativeString = ""
|
||||
var openString = ""
|
||||
var intermediateString = ""
|
||||
var closedString = ""
|
||||
var maybeEscapeNext = false
|
||||
|
||||
|
||||
func addToken( for type : TokenType ) {
|
||||
var inputString : String
|
||||
switch type {
|
||||
case .openTag:
|
||||
inputString = openString
|
||||
case .intermediateTag:
|
||||
inputString = intermediateString
|
||||
case .closeTag:
|
||||
inputString = closedString
|
||||
default:
|
||||
inputString = ""
|
||||
}
|
||||
guard !inputString.isEmpty else {
|
||||
return
|
||||
}
|
||||
guard inputString.count >= rule.minTags else {
|
||||
return
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
openingString = ""
|
||||
}
|
||||
let actualType : TokenType = ( rule.intermediateTag == nil && rule.closingTag == nil ) ? .repeatingTag : type
|
||||
|
||||
var token = Token(type: actualType, inputString: inputString)
|
||||
if rule.closingTag == nil {
|
||||
token.count = inputString.count
|
||||
}
|
||||
|
||||
tokens.append(token)
|
||||
|
||||
switch type {
|
||||
case .openTag:
|
||||
openString = ""
|
||||
case .intermediateTag:
|
||||
intermediateString = ""
|
||||
case .closeTag:
|
||||
closedString = ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Here I am going through and adding the characters in the found set to a cumulative string.
|
||||
// If there is an escape character, then the loop stops and any open tags are tokenised.
|
||||
for char in foundChars {
|
||||
cumulativeString.append(char)
|
||||
if maybeEscapeNext {
|
||||
|
||||
var escaped = cumulativeString
|
||||
if String(char) == rule.openTag || String(char) == rule.intermediateTag || String(char) == rule.closingTag {
|
||||
escaped = String(cumulativeString.replacingOccurrences(of: String(rule.escapeCharacter ?? Character("")), with: ""))
|
||||
}
|
||||
|
||||
openingString.append(escaped)
|
||||
cumulativeString = ""
|
||||
maybeEscapeNext = false
|
||||
}
|
||||
if let existentEscape = rule.escapeCharacter {
|
||||
if cumulativeString == String(existentEscape) {
|
||||
maybeEscapeNext = true
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cumulativeString == rule.openTag {
|
||||
openString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = true
|
||||
} else if cumulativeString == rule.intermediateTag, openTagFound {
|
||||
intermediateString.append(cumulativeString)
|
||||
cumulativeString = ""
|
||||
} else if cumulativeString == rule.closingTag, openTagFound {
|
||||
closedString.append(char)
|
||||
cumulativeString = ""
|
||||
openTagFound = false
|
||||
}
|
||||
}
|
||||
// If we're here, it means that an escape character was found but without a corresponding
|
||||
// tag, which means it might belong to a different rule.
|
||||
// It should be added to the next group of regular characters
|
||||
|
||||
addToken(for: .openTag)
|
||||
addToken(for: .intermediateTag)
|
||||
addToken(for: .closeTag)
|
||||
openingString.append( cumulativeString )
|
||||
}
|
||||
|
||||
if !openingString.isEmpty {
|
||||
tokens.append(Token(type: .string, inputString: "\(openingString)"))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func validateSpacing( nextCharacter : String?, previousCharacter : String?, with rule : CharacterRule ) -> Bool {
|
||||
switch rule.spacesAllowed {
|
||||
case .leadingSide:
|
||||
guard nextCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if nextCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .trailingSide:
|
||||
guard previousCharacter != nil else {
|
||||
return true
|
||||
}
|
||||
if previousCharacter == " " {
|
||||
return false
|
||||
}
|
||||
case .no:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, nil), ( " ", _ ), ( _, " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
case .oneSide:
|
||||
switch (previousCharacter, nextCharacter) {
|
||||
case (nil, " " ), (" ", nil), (" ", " " ):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
|
||||
variant_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"AppCenter.xcframework/ios-arm64_arm64e_armv7_armv7s")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenter.xcframework/ios-arm64_i386_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
"AppCenter.xcframework/ios-arm64_x86_64-maccatalyst")
|
||||
echo "maccatalyst"
|
||||
;;
|
||||
"AppCenter.xcframework/macos-arm64_x86_64")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenter.xcframework/tvos-arm64")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenter.xcframework/tvos-arm64_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_arm64e_armv7_armv7s")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_i386_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_x86_64-maccatalyst")
|
||||
echo "maccatalyst"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/macos-arm64_x86_64")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/tvos-arm64")
|
||||
echo ""
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/tvos-arm64_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
archs_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"AppCenter.xcframework/ios-arm64_arm64e_armv7_armv7s")
|
||||
echo "arm64 arm64e armv7 armv7s"
|
||||
;;
|
||||
"AppCenter.xcframework/ios-arm64_i386_x86_64-simulator")
|
||||
echo "arm64 i386 x86_64"
|
||||
;;
|
||||
"AppCenter.xcframework/ios-arm64_x86_64-maccatalyst")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
"AppCenter.xcframework/macos-arm64_x86_64")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
"AppCenter.xcframework/tvos-arm64")
|
||||
echo "arm64"
|
||||
;;
|
||||
"AppCenter.xcframework/tvos-arm64_x86_64-simulator")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_arm64e_armv7_armv7s")
|
||||
echo "arm64 arm64e armv7 armv7s"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_i386_x86_64-simulator")
|
||||
echo "arm64 i386 x86_64"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/ios-arm64_x86_64-maccatalyst")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/macos-arm64_x86_64")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/tvos-arm64")
|
||||
echo "arm64"
|
||||
;;
|
||||
"AppCenterCrashes.xcframework/tvos-arm64_x86_64-simulator")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
copy_dir()
|
||||
{
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||
}
|
||||
|
||||
SELECT_SLICE_RETVAL=""
|
||||
|
||||
select_slice() {
|
||||
local xcframework_name="$1"
|
||||
xcframework_name="${xcframework_name##*/}"
|
||||
local paths=("${@:2}")
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
local target_path=""
|
||||
|
||||
# Split archs on space so we can find a slice that has all the needed archs
|
||||
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||
|
||||
local target_variant=""
|
||||
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||
target_variant="simulator"
|
||||
fi
|
||||
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||
target_variant="maccatalyst"
|
||||
fi
|
||||
for i in ${!paths[@]}; do
|
||||
local matched_all_archs="1"
|
||||
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
for target_arch in $target_archs; do
|
||||
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
|
||||
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$matched_all_archs" == "1" ]]; then
|
||||
# Found a matching slice
|
||||
echo "Selected xcframework slice ${paths[$i]}"
|
||||
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
install_xcframework() {
|
||||
local basepath="$1"
|
||||
local name="$2"
|
||||
local package_type="$3"
|
||||
local paths=("${@:4}")
|
||||
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
select_slice "${basepath}" "${paths[@]}"
|
||||
local target_path="$SELECT_SLICE_RETVAL"
|
||||
if [[ -z "$target_path" ]]; then
|
||||
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||
return
|
||||
fi
|
||||
local source="$basepath/$target_path"
|
||||
|
||||
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||
|
||||
if [ ! -d "$destination" ]; then
|
||||
mkdir -p "$destination"
|
||||
fi
|
||||
|
||||
copy_dir "$source/" "$destination"
|
||||
echo "Copied $source to $destination"
|
||||
}
|
||||
|
||||
install_xcframework "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenter.xcframework" "AppCenter/Core" "framework" "macos-arm64_x86_64"
|
||||
install_xcframework "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenterCrashes.xcframework" "AppCenter/Crashes" "framework" "macos-arm64_x86_64"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AppCenter
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenter.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenterCrashes.xcframework/macos-arm64_x86_64"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"z" -framework "AppKit" -framework "Foundation" -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AppCenter
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,12 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AppCenter
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenter.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenterCrashes.xcframework/macos-arm64_x86_64"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"z" -framework "AppKit" -framework "Foundation" -framework "SystemConfiguration"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AppCenter
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/LetsMove
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/LetsMove
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,10 +1,13 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/LetsMove
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/LetsMove
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -435,11 +435,11 @@ IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
## SwiftyMarkdown
|
||||
## SwiftyBeaver
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
Copyright (c) 2015 Sebastian Kreutzberger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -372,7 +372,7 @@ Section 8 -- Interpretation.
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the Licensor. The text of the Creative Commons
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
@@ -397,7 +397,7 @@ public licenses.
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2006-2013 Andy Matuschak.
|
||||
Copyright (c) 2009-2013 Elgato Systems GmbH.
|
||||
Copyright (c) 2011-2014 Kornel Lesiski.
|
||||
Copyright (c) 2011-2014 Kornel Lesiński.
|
||||
Copyright (c) 2015-2017 Mayur Pawashe.
|
||||
Copyright (c) 2014 C.W. Betts.
|
||||
Copyright (c) 2014 Petroules Corporation.
|
||||
@@ -468,7 +468,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
<key>FooterText</key>
|
||||
<string>The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Simon Fairbairn
|
||||
Copyright (c) 2015 Sebastian Kreutzberger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -491,7 +491,7 @@ SOFTWARE.
|
||||
<key>License</key>
|
||||
<string>MIT</string>
|
||||
<key>Title</key>
|
||||
<string>SwiftyMarkdown</string>
|
||||
<string>SwiftyBeaver</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
|
||||
@@ -18,10 +18,9 @@ echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}"
|
||||
SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"
|
||||
SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"
|
||||
BCSYMBOLMAP_DIR="BCSymbolMaps"
|
||||
|
||||
# Used as a return value for each invocation of `strip_invalid_archs` function.
|
||||
STRIP_BINARY_RETVAL=0
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
@@ -42,7 +41,17 @@ install_framework()
|
||||
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source="$(readlink "${source}")"
|
||||
source="$(readlink -f "${source}")"
|
||||
fi
|
||||
|
||||
if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then
|
||||
# Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied
|
||||
find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do
|
||||
echo "Installing $f"
|
||||
install_bcsymbolmap "$f" "$destination"
|
||||
rm "$f"
|
||||
done
|
||||
rmdir "${source}/${BCSYMBOLMAP_DIR}"
|
||||
fi
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
@@ -80,7 +89,6 @@ install_framework()
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Copies and strips a vendored dSYM
|
||||
install_dsym() {
|
||||
local source="$1"
|
||||
@@ -95,22 +103,55 @@ install_dsym() {
|
||||
binary_name="$(ls "$source/Contents/Resources/DWARF")"
|
||||
binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}"
|
||||
|
||||
# Strip invalid architectures so "fat" simulator / device frameworks work on device
|
||||
# Strip invalid architectures from the dSYM.
|
||||
if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
|
||||
strip_invalid_archs "$binary" "$warn_missing_arch"
|
||||
fi
|
||||
|
||||
if [[ $STRIP_BINARY_RETVAL == 1 ]]; then
|
||||
if [[ $STRIP_BINARY_RETVAL == 0 ]]; then
|
||||
# Move the stripped file into its final destination.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}"
|
||||
else
|
||||
# The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing.
|
||||
mkdir -p "${DWARF_DSYM_FOLDER_PATH}"
|
||||
touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Used as a return value for each invocation of `strip_invalid_archs` function.
|
||||
STRIP_BINARY_RETVAL=0
|
||||
|
||||
# Strip invalid architectures
|
||||
strip_invalid_archs() {
|
||||
binary="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
# Get architectures for current target binary
|
||||
binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
|
||||
# Intersect them with the architectures we are building for
|
||||
intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
|
||||
# If there are no archs supported by this binary then warn the user
|
||||
if [[ -z "$intersected_archs" ]]; then
|
||||
if [[ "$warn_missing_arch" == "true" ]]; then
|
||||
echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=1
|
||||
return
|
||||
fi
|
||||
stripped=""
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
if [[ "$stripped" ]]; then
|
||||
echo "Stripped $binary of architectures:$stripped"
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=0
|
||||
}
|
||||
|
||||
# Copies the bcsymbolmap files of a vendored framework
|
||||
install_bcsymbolmap() {
|
||||
local bcsymbolmap_path="$1"
|
||||
@@ -134,81 +175,17 @@ code_sign_if_enabled() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Strip invalid architectures
|
||||
strip_invalid_archs() {
|
||||
binary="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
# Get architectures for current target binary
|
||||
binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
|
||||
# Intersect them with the architectures we are building for
|
||||
intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
|
||||
# If there are no archs supported by this binary then warn the user
|
||||
if [[ -z "$intersected_archs" ]]; then
|
||||
if [[ "$warn_missing_arch" == "true" ]]; then
|
||||
echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=0
|
||||
return
|
||||
fi
|
||||
stripped=""
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
if [[ "$stripped" ]]; then
|
||||
echo "Stripped $binary of architectures:$stripped"
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=1
|
||||
}
|
||||
|
||||
install_artifact() {
|
||||
artifact="$1"
|
||||
base="$(basename "$artifact")"
|
||||
case $base in
|
||||
*.framework)
|
||||
install_framework "$artifact"
|
||||
;;
|
||||
*.dSYM)
|
||||
# Suppress arch warnings since XCFrameworks will include many dSYM files
|
||||
install_dsym "$artifact" "false"
|
||||
;;
|
||||
*.bcsymbolmap)
|
||||
install_bcsymbolmap "$artifact"
|
||||
;;
|
||||
*)
|
||||
echo "error: Unrecognized artifact "$artifact""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
copy_artifacts() {
|
||||
file_list="$1"
|
||||
while read artifact; do
|
||||
install_artifact "$artifact"
|
||||
done <$file_list
|
||||
}
|
||||
|
||||
ARTIFACT_LIST_FILE="${BUILT_PRODUCTS_DIR}/cocoapods-artifacts-${CONFIGURATION}.txt"
|
||||
if [ -r "${ARTIFACT_LIST_FILE}" ]; then
|
||||
copy_artifacts "${ARTIFACT_LIST_FILE}"
|
||||
fi
|
||||
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
|
||||
install_dsym "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
install_framework "${PODS_ROOT}/Sparkle/Sparkle.framework"
|
||||
install_dsym "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyBeaver/SwiftyBeaver.framework"
|
||||
fi
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
wait
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenter.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenterCrashes.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/Sparkle"
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' @loader_path/../Frameworks
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown" -framework "SystemConfiguration"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,12 +1,16 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenter.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple/AppCenterCrashes.xcframework/macos-arm64_x86_64" "${PODS_ROOT}/Sparkle"
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver" "${PODS_ROOT}/AppCenter/AppCenter-SDK-Apple" "${PODS_ROOT}/Sparkle" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Core" "${PODS_XCFRAMEWORKS_BUILD_DIR}/AppCenter/Crashes"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown/SwiftyMarkdown.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' @loader_path/../Frameworks
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyMarkdown" -framework "SystemConfiguration"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/LetsMove/LetsMove.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver/SwiftyBeaver.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" @loader_path/../Frameworks
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"z" -framework "AppCenter" -framework "AppCenterCrashes" -framework "AppKit" -framework "Carbon" -framework "Cocoa" -framework "Foundation" -framework "LetsMove" -framework "ShortcutRecorder" -framework "Sparkle" -framework "SwiftyBeaver" -framework "SystemConfiguration"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AppCenter" "-F${PODS_CONFIGURATION_BUILD_DIR}/LetsMove" "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder" "-F${PODS_CONFIGURATION_BUILD_DIR}/Sparkle" "-F${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.0</string>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -0,0 +1,344 @@
|
||||
# Acknowledgements
|
||||
This application makes use of the following third party libraries:
|
||||
|
||||
## ShortcutRecorder
|
||||
|
||||
Creative Commons Attribution 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are
|
||||
granted the Licensed Rights in consideration of Your acceptance of
|
||||
these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the
|
||||
Licensed Material available under these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
d. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
f. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
g. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
i. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
j. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
k. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the Adapted
|
||||
Material from complying with this Public License.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Generated by CocoaPods - https://cocoapods.org
|
||||
@@ -0,0 +1,376 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>This application makes use of the following third party libraries:</string>
|
||||
<key>Title</key>
|
||||
<string>Acknowledgements</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Creative Commons Attribution 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are
|
||||
granted the Licensed Rights in consideration of Your acceptance of
|
||||
these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the
|
||||
Licensed Material available under these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
d. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
f. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
g. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
i. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
j. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
k. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the Adapted
|
||||
Material from complying with this Public License.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>CC BY 4.0</string>
|
||||
<key>Title</key>
|
||||
<string>ShortcutRecorder</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Generated by CocoaPods - https://cocoapods.org</string>
|
||||
<key>Title</key>
|
||||
<string></string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>StringsTable</key>
|
||||
<string>Acknowledgements</string>
|
||||
<key>Title</key>
|
||||
<string>Acknowledgements</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_Pods_unit_tests : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_Pods_unit_tests
|
||||
@end
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then
|
||||
# If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy
|
||||
# frameworks to, so exit 0 (signalling the script phase was successful).
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}"
|
||||
SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"
|
||||
BCSYMBOLMAP_DIR="BCSymbolMaps"
|
||||
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
# Copies and strips a vendored framework
|
||||
install_framework()
|
||||
{
|
||||
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
|
||||
local source="${BUILT_PRODUCTS_DIR}/$1"
|
||||
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
|
||||
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
|
||||
elif [ -r "$1" ]; then
|
||||
local source="$1"
|
||||
fi
|
||||
|
||||
local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source="$(readlink -f "${source}")"
|
||||
fi
|
||||
|
||||
if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then
|
||||
# Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied
|
||||
find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do
|
||||
echo "Installing $f"
|
||||
install_bcsymbolmap "$f" "$destination"
|
||||
rm "$f"
|
||||
done
|
||||
rmdir "${source}/${BCSYMBOLMAP_DIR}"
|
||||
fi
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
|
||||
|
||||
local basename
|
||||
basename="$(basename -s .framework "$1")"
|
||||
binary="${destination}/${basename}.framework/${basename}"
|
||||
|
||||
if ! [ -r "$binary" ]; then
|
||||
binary="${destination}/${basename}"
|
||||
elif [ -L "${binary}" ]; then
|
||||
echo "Destination binary is symlinked..."
|
||||
dirname="$(dirname "${binary}")"
|
||||
binary="${dirname}/$(readlink "${binary}")"
|
||||
fi
|
||||
|
||||
# Strip invalid architectures so "fat" simulator / device frameworks work on device
|
||||
if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
|
||||
strip_invalid_archs "$binary"
|
||||
fi
|
||||
|
||||
# Resign the code if required by the build settings to avoid unstable apps
|
||||
code_sign_if_enabled "${destination}/$(basename "$1")"
|
||||
|
||||
# Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
|
||||
if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
|
||||
local swift_runtime_libs
|
||||
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
|
||||
for lib in $swift_runtime_libs; do
|
||||
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
code_sign_if_enabled "${destination}/${lib}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
# Copies and strips a vendored dSYM
|
||||
install_dsym() {
|
||||
local source="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
if [ -r "$source" ]; then
|
||||
# Copy the dSYM into the targets temp dir.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}"
|
||||
|
||||
local basename
|
||||
basename="$(basename -s .dSYM "$source")"
|
||||
binary_name="$(ls "$source/Contents/Resources/DWARF")"
|
||||
binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}"
|
||||
|
||||
# Strip invalid architectures from the dSYM.
|
||||
if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
|
||||
strip_invalid_archs "$binary" "$warn_missing_arch"
|
||||
fi
|
||||
if [[ $STRIP_BINARY_RETVAL == 0 ]]; then
|
||||
# Move the stripped file into its final destination.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}"
|
||||
else
|
||||
# The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing.
|
||||
mkdir -p "${DWARF_DSYM_FOLDER_PATH}"
|
||||
touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Used as a return value for each invocation of `strip_invalid_archs` function.
|
||||
STRIP_BINARY_RETVAL=0
|
||||
|
||||
# Strip invalid architectures
|
||||
strip_invalid_archs() {
|
||||
binary="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
# Get architectures for current target binary
|
||||
binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
|
||||
# Intersect them with the architectures we are building for
|
||||
intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
|
||||
# If there are no archs supported by this binary then warn the user
|
||||
if [[ -z "$intersected_archs" ]]; then
|
||||
if [[ "$warn_missing_arch" == "true" ]]; then
|
||||
echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=1
|
||||
return
|
||||
fi
|
||||
stripped=""
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
if [[ "$stripped" ]]; then
|
||||
echo "Stripped $binary of architectures:$stripped"
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=0
|
||||
}
|
||||
|
||||
# Copies the bcsymbolmap files of a vendored framework
|
||||
install_bcsymbolmap() {
|
||||
local bcsymbolmap_path="$1"
|
||||
local destination="${BUILT_PRODUCTS_DIR}"
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"
|
||||
}
|
||||
|
||||
# Signs a framework with the provided identity
|
||||
code_sign_if_enabled() {
|
||||
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
|
||||
# Use the current code_sign_identity
|
||||
echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
|
||||
local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'"
|
||||
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
code_sign_cmd="$code_sign_cmd &"
|
||||
fi
|
||||
echo "$code_sign_cmd"
|
||||
eval "$code_sign_cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/ShortcutRecorder/ShortcutRecorder.framework"
|
||||
fi
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
wait
|
||||
fi
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
FOUNDATION_EXPORT double Pods_unit_testsVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char Pods_unit_testsVersionString[];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
|
||||
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "ShortcutRecorder"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -0,0 +1,6 @@
|
||||
framework module Pods_unit_tests {
|
||||
umbrella header "Pods-unit-tests-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder/ShortcutRecorder.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
|
||||
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa" -framework "ShortcutRecorder"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/ShortcutRecorder
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,11 +1,14 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ShortcutRecorder
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_LDFLAGS = $(inherited) -framework "Carbon" -framework "Cocoa"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/ShortcutRecorder
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
# Used as a return value for each invocation of `strip_invalid_archs` function.
|
||||
STRIP_BINARY_RETVAL=0
|
||||
|
||||
# Strip invalid architectures
|
||||
strip_invalid_archs() {
|
||||
binary="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
# Get architectures for current target binary
|
||||
binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
|
||||
# Intersect them with the architectures we are building for
|
||||
intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
|
||||
# If there are no archs supported by this binary then warn the user
|
||||
if [[ -z "$intersected_archs" ]]; then
|
||||
if [[ "$warn_missing_arch" == "true" ]]; then
|
||||
echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=1
|
||||
return
|
||||
fi
|
||||
stripped=""
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
if [[ "$stripped" ]]; then
|
||||
echo "Stripped $binary of architectures:$stripped"
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=0
|
||||
}
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
# Copies and strips a vendored dSYM
|
||||
install_dsym() {
|
||||
local source="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
if [ -r "$source" ]; then
|
||||
# Copy the dSYM into the targets temp dir.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}"
|
||||
|
||||
local basename
|
||||
basename="$(basename -s .dSYM "$source")"
|
||||
binary_name="$(ls "$source/Contents/Resources/DWARF")"
|
||||
binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}"
|
||||
|
||||
# Strip invalid architectures from the dSYM.
|
||||
if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
|
||||
strip_invalid_archs "$binary" "$warn_missing_arch"
|
||||
fi
|
||||
if [[ $STRIP_BINARY_RETVAL == 0 ]]; then
|
||||
# Move the stripped file into its final destination.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}"
|
||||
else
|
||||
# The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing.
|
||||
mkdir -p "${DWARF_DSYM_FOLDER_PATH}"
|
||||
touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Copies the bcsymbolmap files of a vendored framework
|
||||
install_bcsymbolmap() {
|
||||
local bcsymbolmap_path="$1"
|
||||
local destination="${BUILT_PRODUCTS_DIR}"
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"
|
||||
}
|
||||
|
||||
install_dsym "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM"
|
||||
@@ -1,3 +1,4 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Sparkle
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Sparkle"
|
||||
@@ -5,8 +6,10 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/../Frameworks
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Sparkle
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,3 +1,4 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Sparkle
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Sparkle"
|
||||
@@ -5,8 +6,10 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/../Frameworks
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Sparkle
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.9.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_SwiftyBeaver : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_SwiftyBeaver
|
||||
@end
|
||||
@@ -11,6 +11,6 @@
|
||||
#endif
|
||||
|
||||
|
||||
FOUNDATION_EXPORT double SwiftyMarkdownVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char SwiftyMarkdownVersionString[];
|
||||
FOUNDATION_EXPORT double SwiftyBeaverVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char SwiftyBeaverVersionString[];
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyMarkdown
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyBeaver
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -0,0 +1,6 @@
|
||||
framework module SwiftyBeaver {
|
||||
umbrella header "SwiftyBeaver-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CODE_SIGN_IDENTITY =
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyMarkdown
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyBeaver
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyMarkdown
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftyBeaver
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,5 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_SwiftyMarkdown : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_SwiftyMarkdown
|
||||
@end
|
||||
@@ -1,6 +0,0 @@
|
||||
framework module SwiftyMarkdown {
|
||||
umbrella header "SwiftyMarkdown-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
@@ -5,3 +5,10 @@
|
||||
**AltTab** brings the power of Windows alt-tab to macOS
|
||||
|
||||
[Find out more on the official website](https://alt-tab-macos.netlify.app/)
|
||||
|
||||
<div align="right">
|
||||
<p>Project supported by</p>
|
||||
<a href="https://jb.gg/OpenSource">
|
||||
<img src="docs/public/demo/jetbrains.svg" alt="Jetbrains" width="149" height="32">
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -37,8 +37,9 @@
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugDocumentVersioning = "NO"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
@@ -50,6 +51,12 @@
|
||||
ReferencedContainer = "container:alt-tab-macos.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "--logs=info"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<LocationScenarioReference
|
||||
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
|
||||
referenceType = "1">
|
||||
@@ -61,6 +68,16 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D04BA5E73DAF232ADE11C97A"
|
||||
BuildableName = "AltTab.app"
|
||||
BlueprintName = "alt-tab-macos"
|
||||
ReferencedContainer = "container:alt-tab-macos.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF0C873EC188A7EBBCF4310D"
|
||||
BuildableName = "unit-tests.xctest"
|
||||
BlueprintName = "unit-tests"
|
||||
ReferencedContainer = "container:alt-tab-macos.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = ""
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF0C873EC188A7EBBCF4310D"
|
||||
BuildableName = "unit-tests.xctest"
|
||||
BlueprintName = "unit-tests"
|
||||
ReferencedContainer = "container:alt-tab-macos.xcodeproj">
|
||||
</BuildableReference>
|
||||
<LocationScenarioReference
|
||||
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
|
||||
referenceType = "1">
|
||||
</LocationScenarioReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<LocationScenarioReference
|
||||
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
|
||||
referenceType = "1">
|
||||
</LocationScenarioReference>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -3,7 +3,8 @@
|
||||
PRODUCT_NAME = AltTab
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lwouis.alt-tab-macos
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12
|
||||
SWIFT_VERSION = 5.0
|
||||
SWIFT_VERSION = 5.8 // xcode 14.2 translates this to 5.0; xcode 16.2 as 5.8
|
||||
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
|
||||
INFOPLIST_FILE = Info.plist
|
||||
CODE_SIGN_ENTITLEMENTS = alt_tab_macos.entitlements
|
||||
FRAMEWORK_SEARCH_PATHS[config=*] = $(inherited) /System/Library/PrivateFrameworks // for SkyLight.framework
|
||||
@@ -14,3 +15,5 @@ PLIST_FILE_OUTPUT_FORMAT = binary
|
||||
COCOAPODS_PARALLEL_CODE_SIGN = YES // codesign pods faster
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES
|
||||
SWIFT_OBJC_BRIDGING_HEADER = alt-tab-macos-Bridging-Header.h
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES
|
||||
DEAD_CODE_STRIPPING = YES
|
||||
@@ -7,3 +7,4 @@ CODE_SIGN_IDENTITY = Local Self-Signed
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG // for macro `#if DEBUG`
|
||||
OTHER_SWIFT_FLAGS = -driver-time-compilation -Xfrontend -warn-long-function-bodies=250 -Xfrontend -warn-long-expression-type-checking=250 // find slow compilation areas
|
||||
OTHER_CODE_SIGN_FLAGS = --timestamp=none
|
||||
SWIFT_COMPILATION_MODE = incremental
|
||||
@@ -6,3 +6,5 @@
|
||||
CODE_SIGN_IDENTITY = Developer ID Application: Louis Pontoise (QXD7GW8FHY)
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO // for notarization
|
||||
OTHER_CODE_SIGN_FLAGS = --timestamp --deep --options runtime // for notarization
|
||||
SWIFT_COMPILATION_MODE = wholemodule
|
||||
SWIFT_OPTIMIZATION_LEVEL = -O
|
||||
@@ -0,0 +1,12 @@
|
||||
// docs: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
|
||||
|
||||
SWIFT_VERSION = 5.8
|
||||
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES
|
||||
CODE_SIGNING_REQUIRED = NO // no need for codesign
|
||||
CODE_SIGNING_ALLOWED = NO // no need for codesign
|
||||
ONLY_ACTIVE_ARCH = YES // only need to run on the host machine
|
||||
ENABLE_BITCODE = NO
|
||||
GCC_GENERATE_TEST_COVERAGE_FILES = NO
|
||||
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO
|
||||
TEST_ATTACHMENT_LOGGING = NO
|
||||
DEAD_CODE_STRIPPING = YES
|
||||
@@ -0,0 +1,8 @@
|
||||
// docs: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
|
||||
|
||||
#include "Pods/Target Support Files/Pods-unit-tests/Pods-unit-tests.debug.xcconfig"
|
||||
#include "test-base.xcconfig"
|
||||
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG // for macro `#if DEBUG`
|
||||
OTHER_SWIFT_FLAGS = -driver-time-compilation -Xfrontend -warn-long-function-bodies=250 -Xfrontend -warn-long-expression-type-checking=250 // find slow compilation areas
|
||||
SWIFT_COMPILATION_MODE = incremental
|
||||
@@ -0,0 +1,7 @@
|
||||
// docs: https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6
|
||||
|
||||
#include "Pods/Target Support Files/Pods-unit-tests/Pods-unit-tests.release.xcconfig"
|
||||
#include "test-base.xcconfig"
|
||||
|
||||
DEBUG_INFORMATION_FORMAT = none // we don't need dSYM files
|
||||
GCC_GENERATE_DEBUGGING_SYMBOLS = NO // we don't need dSYM files
|
||||
@@ -3,13 +3,16 @@
|
||||
These third-party libraries are used:
|
||||
|
||||
**ShortcutRecorder**
|
||||
[Website](https://github.com/Kentzo/ShortcutRecorder) - [CC 4.0 license](https://github.com/Kentzo/ShortcutRecorder/blob/master/LICENSE.txt)
|
||||
[Website](https://github.com/Kentzo/ShortcutRecorder) - [CC 4.0 License](https://github.com/Kentzo/ShortcutRecorder/blob/master/LICENSE.txt)
|
||||
|
||||
**Sparkle**
|
||||
[Website](https://github.com/sparkle-project/Sparkle) - [License](https://github.com/sparkle-project/Sparkle/blob/master/LICENSE)
|
||||
|
||||
**SwiftyMarkdown**
|
||||
[Website](https://github.com/SimonFairbairn/SwiftyMarkdown) - [MIT License](https://github.com/SimonFairbairn/SwiftyMarkdown/blob/master/LICENSE)
|
||||
|
||||
**LetsMove**
|
||||
[Website](https://github.com/potionfactory/LetsMove) - [Public domain](https://github.com/potionfactory/LetsMove#license)
|
||||
[Website](https://github.com/potionfactory/LetsMove) - [Public domain](https://github.com/potionfactory/LetsMove#license)
|
||||
|
||||
**AppCenter**
|
||||
[Website](https://github.com/microsoft/appcenter-sdk-apple) - [Custom](https://github.com/microsoft/appcenter-sdk-apple/blob/develop/LICENSE)
|
||||
|
||||
**SwiftyBeaver**
|
||||
[Website](https://github.com/SwiftyBeaver/SwiftyBeaver) - [MIT License](https://github.com/SwiftyBeaver/SwiftyBeaver/blob/master/LICENSE)
|
||||
@@ -2,41 +2,18 @@
|
||||
layout: bare
|
||||
---
|
||||
|
||||
<h1><a style="
|
||||
position: fixed;
|
||||
top: 1.5em;
|
||||
right: 10px;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.15em 0.9em;
|
||||
font-size: 0.7em;
|
||||
background: #ffdbd5;
|
||||
/*background: #f91675;*/
|
||||
color: #950020;
|
||||
/*color: white;*/
|
||||
font-weight: normal;
|
||||
" href="https://alt-tab-macos.netlify.app/support">Support this project ❤️</a></h1>
|
||||
|
||||
{% include_relative Changelog.md %}
|
||||
|
||||
<style>#google_translate_element,.skiptranslate{display:none;}body{top:0!important;}</style>
|
||||
<div id="google_translate_element"></div>
|
||||
<script>
|
||||
function getUserLanguage() {
|
||||
if (navigator.languages && navigator.languages.length > 0) {
|
||||
// Chinese is the only exception where google translate needs the full code
|
||||
// see https://sites.google.com/site/opti365/translate_codes
|
||||
const l = navigator.languages[0]
|
||||
if (l.length > 2 && l !== "zh-CN" && l !== "zh-TW") {
|
||||
return l.slice(0, 2)
|
||||
}
|
||||
return l
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
function googleTranslateElementInit() {
|
||||
new google.translate.TranslateElement({
|
||||
pageLanguage: 'en',
|
||||
includedLanguages: getUserLanguage(),
|
||||
autoDisplay: false
|
||||
}, 'google_translate_element')
|
||||
setTimeout(() => {
|
||||
var a = document.querySelector("#google_translate_element select")
|
||||
a.selectedIndex = 1
|
||||
a.dispatchEvent(new Event('change'))
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const userLanguage = getUserLanguage()
|
||||
if (userLanguage !== "en") {
|
||||
let script = document.createElement("script")
|
||||
script.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
</script>
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
There are many ways to contribute:
|
||||
|
||||
* Come discuss with us on the [Discord server](https://discord.gg/CVEPeDufJa).
|
||||
* [Suggest an enhancement or discuss an issue on github](https://github.com/lwouis/alt-tab-macos/issues), or use the feedback form in the app.
|
||||
* [Localize the app in your language](https://poeditor.com/join/project/8AOEZ0eAZE)
|
||||
|
||||
## Technical overview
|
||||
|
||||
This document gives an technical overview of the project, for newcomers who want to contribute.
|
||||
This document gives a technical overview of the project, for newcomers who want to contribute.
|
||||
|
||||
## Building the project locally
|
||||
|
||||
@@ -31,13 +30,13 @@ Documentation is abysmal. Very simple things are not documented at all, and good
|
||||
|
||||
Dependencies were historically never handled by Apple. The community came up with [Cocoapods](https://cocoapods.org/) which is the de-facto dependency manager for Apple ecosystem projects these days, even though Apple is now trying to push their own.
|
||||
|
||||
OS APIs are quite limited for the kind of low-level, system-wide app AltTab is. This means often we just don’t have an API to do something. For instance, there is no API to ask the OS “how many Spaces does the user have?” or “Can you focus the window on Space 2?”. There are however, retro-engineered private APIs which you can call. These are not documented at all, not guaranteed to be there in future macOS releases, and prevent us from releasing AltTab on the Mac AppStore. We have tried my best to [document](https://github.com/lwouis/alt-tab-macos/blob/master/src/api-wrappers/PrivateApis.swift) the ones we are using, as well as ones we investigated in the past.
|
||||
OS APIs are quite limited for the kind of low-level, system-wide app AltTab is. This means often we just don’t have an API to do something. For instance, there is no API to ask the OS “how many Spaces does the user have?” or “Can you focus the window on Space 2?”. There are however, retro-engineered private APIs which you can call. These are not documented at all, not guaranteed to be there in future macOS releases, and prevent us from releasing AltTab on the Mac AppStore. We have tried our best to [document the ones we are using](https://github.com/lwouis/alt-tab-macos/blob/master/src/api-wrappers/private-apis/README.md), as well as [the ones we investigated](https://github.com/lwouis/alt-tab-macos/blob/master/src/experimentations/PrivateApis.swift) in the past.
|
||||
|
||||
## This project specifically
|
||||
|
||||
To mitigate the issues listed above, we took some measures.
|
||||
|
||||
We minimize reliance on XCode, InterfaceBuilder, Playground, and other GUI tools. You can’t cut the dependency completely though as only XCode can build macos apps. Currently the project has these files:
|
||||
We minimize reliance on XCode, InterfaceBuilder, Playground, and other GUI tools. You can’t cut the dependency completely though as only XCode can build macOS apps. Currently, the project has these files:
|
||||
|
||||
* 1 xib (InterfaceBuilder UI file, describing the menubar items like “Edit” or “Format”)
|
||||
* `alt-tab-macos.xcodeproj` file describing AltTab itself. It contains some settings for the app
|
||||
@@ -101,7 +100,7 @@ In an attempt to not have too many regressions, this documents will list OS inte
|
||||
* There is no open window
|
||||
* AltTab should appear on top of all windows, dialogs, pop-overs, the Dock, etc
|
||||
|
||||
### OS events to handle while AltTab's UI is shown
|
||||
### OS events to handle while AltTab’s UI is shown
|
||||
|
||||
* An app is launching/quitting
|
||||
* A new window opens
|
||||
@@ -109,12 +108,14 @@ In an attempt to not have too many regressions, this documents will list OS inte
|
||||
|
||||
### Drag-and-drop on top of the thumbnails
|
||||
|
||||
* Drag-and-dropping a URL onto a window thumbnail should open it with that window's app
|
||||
* Drag-and-dropping a file onto a window thumbnail should open it with that window's app
|
||||
* Drag-and-dropping a URL onto a window thumbnail should open it with that window’s app
|
||||
* Drag-and-dropping a file onto a window thumbnail should open it with that window’s app
|
||||
|
||||
### System Preferences
|
||||
|
||||
* General > Appearance > "Dark": switches to Dark Mode
|
||||
* General > Accent color > "Graphite": traffic lights on thumbnails should be gray
|
||||
* Accessibility > Display > Reduce transparency: AltTab background should be a solid color
|
||||
* General > Show scroll bars > "Always": regenerates all scrollbars
|
||||
* Display > Resolution > Scaled: changes DPI and rescale AltTab
|
||||
* Mission Control > "Displays have separate Spaces": changes Spaces behavior on multi-displays setups
|
||||
@@ -125,7 +126,7 @@ In an attempt to not have too many regressions, this documents will list OS inte
|
||||
* A window is moved to another space by drag-and-dropping on the Spaces thumbnails at the top of the Mission Control UI
|
||||
* A window is moved to another space by dragging it on the side of the current Space, and waiting for a Space transition, then dropping it
|
||||
* A window is moved to another space by destroying the Space it is in
|
||||
* An app is assigned to a specific space or all spaces by clicking it's Dock icon > Options > Assign to
|
||||
* An app is assigned to a specific space or all spaces by clicking its Dock icon > Options > Assign to
|
||||
|
||||
### Shortcuts
|
||||
|
||||
@@ -151,7 +152,7 @@ In an attempt to not have too many regressions, this documents will list OS inte
|
||||
* Right-to-left languages (e.g. arabic) have the whole layout reversed
|
||||
* For the main window, even navigation is reversed
|
||||
* For preferences and feedback windows, all layout is reversed
|
||||
* Text length can varies per languages which can create layout issues
|
||||
* Text length can vary per languages which can create layout issues
|
||||
|
||||
### Misc
|
||||
|
||||
|
||||
@@ -5,109 +5,290 @@ They helped [develop the app](https://github.com/lwouis/alt-tab-macos/graphs/con
|
||||
* [adamnemecek](https://github.com/adamnemecek)
|
||||
* [AfzalivE](https://github.com/AfzalivE)
|
||||
* [akx](https://github.com/akx)
|
||||
* [Allsochen](https://github.com/Allsochen)
|
||||
* [ayroblu](https://github.com/ayroblu)
|
||||
* [britalmeida](https://github.com/britalmeida)
|
||||
* [Calinou](https://github.com/Calinou)
|
||||
* [damonpam](https://github.com/damonpam)
|
||||
* [gcbw](https://github.com/gcbw)
|
||||
* [decodism](https://github.com/decodism)
|
||||
* [delaneyb](https://github.com/delaneyb)
|
||||
* [gingerr](https://github.com/gingerr)
|
||||
* [GrzegorzKazana](https://github.com/GrzegorzKazana)
|
||||
* [hughlilly](https://github.com/hughlilly)
|
||||
* [i0ntempest](https://github.com/i0ntempest)
|
||||
* [karbassi](https://github.com/karbassi)
|
||||
* [L1cardo](https://github.com/L1cardo)
|
||||
* [lwouis](https://github.com/lwouis)
|
||||
* [metacodes](https://github.com/metacodes)
|
||||
* [mnin](https://github.com/mnin)
|
||||
* [MrPaschenko](https://github.com/MrPaschenko)
|
||||
* [nella17](https://github.com/nella17)
|
||||
* [notlmn](https://github.com/notlmn)
|
||||
* [phungtuanhoang1996](https://github.com/phungtuanhoang1996)
|
||||
* [rbnis](https://github.com/rbnis)
|
||||
* [Rjevski](https://github.com/Rjevski)
|
||||
* [samdenty](https://github.com/samdenty)
|
||||
* [shaqed](https://github.com/shaqed)
|
||||
* [ShlomoCode](https://github.com/ShlomoCode)
|
||||
* [skolj](https://github.com/skolj)
|
||||
* [stopdesign](https://github.com/stopdesign)
|
||||
* [xanathar](https://github.com/xanathar)
|
||||
* [zacharee](https://github.com/zacharee)
|
||||
|
||||
They helped [localize the app](https://poeditor.com/join/project/8AOEZ0eAZE):
|
||||
|
||||
* 73
|
||||
* A
|
||||
* a774 nn
|
||||
* Aamirsuleman
|
||||
* Aarni Koskela
|
||||
* Abdulelah
|
||||
* Abdur-Rahman Bilal
|
||||
* Aden Aziz
|
||||
* Admin
|
||||
* Aindriú Mac Giolla Eoin
|
||||
* Ajaykumarmkaju
|
||||
* Aku
|
||||
* Albert Abdilim
|
||||
* Aleksander Bang-Larsen
|
||||
* Alex
|
||||
* Alexander Kilian
|
||||
* Alexey Eremeev
|
||||
* Ali
|
||||
* Ali Gokmen
|
||||
* Ali. tas103
|
||||
* Allen Guan
|
||||
* Alonso Chen
|
||||
* Ameng
|
||||
* Anders
|
||||
* Andreas Wagner
|
||||
* Andrej Koman
|
||||
* Andrew Vader
|
||||
* antoon334
|
||||
* Anurag Roy
|
||||
* anushree b
|
||||
* Arman
|
||||
* Artem Stankov
|
||||
* Arthur
|
||||
* Ash
|
||||
* blanorama
|
||||
* bluefirex
|
||||
* BO the Man
|
||||
* bovirus
|
||||
* Bufo
|
||||
* BV Joshi
|
||||
* caduellery
|
||||
* Caner İlhan
|
||||
* catcore
|
||||
* changanmoon
|
||||
* Christian Keilmann
|
||||
* Chun Fei Lung
|
||||
* codecantata
|
||||
* codingalone
|
||||
* Cong Tri
|
||||
* Dan
|
||||
* Dan84
|
||||
* Darius Rosendahl
|
||||
* Darko
|
||||
* David R
|
||||
* Denis
|
||||
* Dékány Martin
|
||||
* dibas-np
|
||||
* Didier Deschrijver
|
||||
* Dzhuneyt
|
||||
* edu_sombra
|
||||
* EDUARDO
|
||||
* Eimantas
|
||||
* Eliezer Shpigelman
|
||||
* Emir H. KAYNAK
|
||||
* EP45
|
||||
* Eric WANTZ
|
||||
* Ersagun Kuruca
|
||||
* EUKA
|
||||
* Eukarya
|
||||
* Eyelly wu
|
||||
* fil
|
||||
* Filipe
|
||||
* Frangarciasalomon
|
||||
* Frank
|
||||
* Friendship1
|
||||
* Fukuda
|
||||
* Ganesh Sawant
|
||||
* Gezimos
|
||||
* Giacomo Volpi
|
||||
* Giang
|
||||
* Github
|
||||
* Gkostov
|
||||
* Grzegorz Kazana
|
||||
* Guillaume
|
||||
* Gumpanath Puttarassu
|
||||
* hann-solo
|
||||
* Haoshuai Xu
|
||||
* Hao豪
|
||||
* Heramb Lonkar
|
||||
* Hjörtur Hjartarson
|
||||
* Hokuto Kato
|
||||
* Huandngoc
|
||||
* Huynh Sang
|
||||
* Ialiendeg
|
||||
* Ida Bomholt Dyrholm Jacobsen
|
||||
* Igor Aradski
|
||||
* igorpocta
|
||||
* Indexerrowaty
|
||||
* Inês Almeida
|
||||
* isametry
|
||||
* Ismatulla
|
||||
* Isthereanybody
|
||||
* Jaeyong Sung
|
||||
* Jakob
|
||||
* Jakob Licina
|
||||
* Jakub Bartušek
|
||||
* Jesus Angel Villafuerte
|
||||
* jiho4
|
||||
* Jisan
|
||||
* Joao Vitor Casarin
|
||||
* Jord Nijhuis
|
||||
* Jui Yuan Liu
|
||||
* Jules Beckers
|
||||
* Julian Nowaczyk
|
||||
* Julio
|
||||
* justcontributor
|
||||
* Kagurazaka Tsuki
|
||||
* kal
|
||||
* kant
|
||||
* Kawarimidoll+git
|
||||
* kazuki sakamoto
|
||||
* Kevinsevinche
|
||||
* KillianG
|
||||
* Kinyagulovrr
|
||||
* Klara
|
||||
* Kostiantyn Ko
|
||||
* Kristófer Fannar Björnsson
|
||||
* Kushnee5
|
||||
* Kuuchi
|
||||
* Lakshman Kolappan
|
||||
* Lars
|
||||
* Lasse Mattila
|
||||
* Lcwhhh
|
||||
* Leo
|
||||
* Lester
|
||||
* Logan Boissy
|
||||
* Loic. peron
|
||||
* Loïc
|
||||
* LostInCompilation
|
||||
* Lumaxis
|
||||
* lwouis
|
||||
* M
|
||||
* Malo
|
||||
* Maplevantablack
|
||||
* Marc Pla
|
||||
* Marcus Billman
|
||||
* Marekscholle
|
||||
* Marko McLion
|
||||
* Martin Mitka
|
||||
* Martin. mitka
|
||||
* Masayoshi Motojima
|
||||
* Masih
|
||||
* Matheus
|
||||
* Max
|
||||
* Maximilian Falk
|
||||
* MaximilianFreitag
|
||||
* Mehmet Ali Ertorer
|
||||
* Meriç Bağlayan
|
||||
* Michael
|
||||
* Milos M
|
||||
* Mohammad Al Zouabi
|
||||
* mohma
|
||||
* Mohsin Rahman
|
||||
* Mr. axel. bock
|
||||
* mrtbts
|
||||
* MuDraconis
|
||||
* Muzhenstudent
|
||||
* Mwolfinspace
|
||||
* Nathancodes
|
||||
* Navekar Vishal
|
||||
* NIKHIL S
|
||||
* Nikola Rajić
|
||||
* Nils Fahldieck
|
||||
* Nilton Souza
|
||||
* Nmolham
|
||||
* Nuriddin Islamov
|
||||
* Ori
|
||||
* Paul Pichaureau
|
||||
* Paul Przybyszewski
|
||||
* Paul. pichaureau
|
||||
* Pehovorka
|
||||
* Pendolino
|
||||
* Petar Shomov
|
||||
* Peterkim0620
|
||||
* Petr Kolář
|
||||
* ponchik
|
||||
* Petter Johan Olsen
|
||||
* Piotr Budny
|
||||
* Quentin. douarre
|
||||
* Rajesh Kaswala
|
||||
* rajuneon
|
||||
* Rami Elsawy
|
||||
* Raphaël
|
||||
* Rasmus
|
||||
* Raymonf
|
||||
* raz
|
||||
* Razvan
|
||||
* rbnis
|
||||
* rei fujise
|
||||
* Roccobot
|
||||
* Roccobot
|
||||
* Rodrigo Schneider
|
||||
* Ron Nuss
|
||||
* rPaasch
|
||||
* Ryan Nardi
|
||||
* sa-ha
|
||||
* Sai Khant Zay Lynn Yaung
|
||||
* Sami Suleiman
|
||||
* Saransh
|
||||
* sawtooth
|
||||
* Sebastian
|
||||
* Selcuk Dursun
|
||||
* Sergey
|
||||
* Serhii Alekseev
|
||||
* Seyedparsa Mirtaheri
|
||||
* Shameem Reza
|
||||
* SheNeVmerla
|
||||
* Shivam Bansal
|
||||
* shlomo
|
||||
* Sippawit Thammawiset
|
||||
* Sjur Moshagen
|
||||
* Sjur.n. moshagen
|
||||
* Stan Smits
|
||||
* star-affinity
|
||||
* Stefan
|
||||
* Stefan Lange
|
||||
* Svetoslav Stefanov
|
||||
* Thebennue
|
||||
* Thomas Wölk
|
||||
* Thorsten
|
||||
* Thorsten
|
||||
* Tomoa Nozawa
|
||||
* Tran
|
||||
* Trevor
|
||||
* Tsilimpotis Dimitrios
|
||||
* Umutakkaya1996
|
||||
* Vegard
|
||||
* Vadym
|
||||
* Vijayant Saini
|
||||
* Vincent Orback
|
||||
* Vlad
|
||||
* Webmaster
|
||||
* Wesley Matos
|
||||
* Whatsmine-HaoshuaiXu
|
||||
* White Vogel
|
||||
* Wilhelm Wolfgang Gärtner
|
||||
* Yongsung Yoon
|
||||
* Yorben
|
||||
* Yossi Zahn
|
||||
* ysaito
|
||||
* Yukai
|
||||
* yumechan
|
||||
* Yusuf Caliskan
|
||||
* Zamaialexander
|
||||
* Zuhaib Syed
|
||||
* Τσιλιμπότης Δημ.
|
||||
* Андрій Кирбаба
|
||||
* Ивайло
|
||||
* Пустой
|
||||
* 林清
|
||||
* 橙梓
|
||||
@@ -4,38 +4,63 @@ permalink: /
|
||||
|
||||
# Introduction
|
||||
|
||||
[](public/demo/frontpage.jpg)
|
||||
[<img src="public/demo/frontpage.jpg" alt="Screenshot showing AltTab" width="1920" height="1080" style="width:100%; height:auto;" />](public/demo/frontpage.jpg)
|
||||
|
||||
**AltTab** brings the power of Windows's "alt-tab" window switcher to macOS.
|
||||
Alternative Styles
|
||||
|
||||
| [<img loading="lazy" src="public/demo/app-icons-style.jpg" alt="Screenshot: App Icons Style" width="513" height="241" style="width:100%; height:auto;" />](public/demo/app-icons-style.jpg) | [<img loading="lazy" src="public/demo/titles-style.jpg" alt="Screenshot: Titles Style" width="766" height="248" style="width:100%; height:auto;" />](public/demo/titles-style.jpg) |
|
||||
|
||||
## Installation
|
||||
|
||||
[**Download the latest release**]({{ site.github.latest_release.assets[0].browser_download_url }})
|
||||
* [**Download the latest release**]({{ site.github.latest_release.assets[0].browser_download_url }})
|
||||
* Alternatively, you can use [homebrew](https://brew.sh/): `brew install --cask alt-tab`
|
||||
|
||||
Alternatively, you can use [homebrew](https://brew.sh/):
|
||||
## Features
|
||||
|
||||
Homebrew 2.5 or above | Homebrew 2.4 or below
|
||||
-|-
|
||||
`brew install alt-tab` | `brew cask install alt-tab`
|
||||
* Switch focus to any window
|
||||
* Minimize, close, fullscreen any window; Hide, quit any app
|
||||
* Customize AltTab appearance (pick one of 3 styles, then customize further)
|
||||
* Custom trigger shortcuts with almost any keys
|
||||
* Blacklist apps you don’t want to list or trigger AltTab from
|
||||
* Light and Dark Appearance
|
||||
* Drag-and-drop things on top of window thumbnails
|
||||
* Accessibility: VoiceOver, sticky keys, reduced transparency, etc
|
||||
|
||||
## Compatibility
|
||||
|
||||
* __macOS version:__ from 10.12 to 12 (Monterey)
|
||||
* __Apple Silicon:__ yes, the app is [universal](https://developer.apple.com/documentation/apple-silicon/porting-your-macos-apps-to-apple-silicon)
|
||||
* __macOS version:__ from 10.12 to latest
|
||||
* __Apple Silicon:__ yes, AltTab is [universal](https://developer.apple.com/documentation/apple-silicon/porting-your-macos-apps-to-apple-silicon)
|
||||
|
||||
## Localization
|
||||
|
||||
The app is available in:
|
||||
|
||||
Bahasa Indonesia, Català, Dansk, Deutsch, English, Español, Français, Italiano, Lëtzebuergesch, Magyar, Nederlands, Norsk, Polski, Português, Português (Brasil), Slovenčina, Slovenščina, Suomi, Svenska, Tiếng Việt, Türkçe, Čeština, Български, Русский язык, Српски / Srpski, українська мова, العربية, فارسی, हिन्दी, 日本語, 简体中文, 繁體中文, 한국어
|
||||
AltTab is available in many languages: Bahasa Indonesia, Basa Jawa, Català, Čeština, Dansk, Deutsch, Eesti keel, English, Español, Français, Gaeilge, Galego, Hrvatski, Íslenska, Italiano, Kurdî, Lietuvių, Lëtzebuergesch, Magyar, Nederlands, Norsk Bokmål, Norsk Nynorsk, Oʻzbekcha, Polski, Português, Português (Brasil), Română, Shqip, Slovenčina, Slovenščina, Suomi, Svenska, Tiếng Việt, Türkçe, Български, Русский, Српски, Українська, Ελληνικά, עִבְרִית, العربية, فارسی, हिन्दी, বাংলা, ગુજરાતી, தமிழ், ಕನ್ನಡ, മലയാളം, ภาษาไทย, 日本語, 简体中文, 繁體中文, 繁體中文 (香港), 한국어
|
||||
|
||||
[Contribute your own language easily!](https://poeditor.com/join/project/8AOEZ0eAZE)
|
||||
|
||||
## Privacy and respecting the user
|
||||
|
||||
* AltTab doesn’t upload or download any data without the user explicitly asking for it (e.g. asking to update the app, sending a crash report, etc)
|
||||
* AltTab uses as few resources as possible: CPU, memory, disk, etc. The app footprint is small (e.g. small disk size, minimal CPU use in the background, etc)
|
||||
|
||||
## Configuration
|
||||
|
||||
Change the shortcut keys, switch to a Windows theme and more, using the Preferences window:
|
||||
|
||||
| [](public/demo/preferences1.png)| [](public/demo/preferences2.png) |
|
||||
| [<img loading="lazy" src="public/demo/preferences-general.jpg" alt="Screenshot: General preferences" width="672" height="354" style="width:100%; height:auto;" />](public/demo/preferences-general.jpg) | [<img loading="lazy" src="public/demo/preferences-controls.jpg" alt="Screenshot: Controls preferences" width="677" height="688" style="width:100%; height:auto;" />](public/demo/preferences-controls.jpg) |
|
||||
| [<img loading="lazy" src="public/demo/preferences-appearance.jpg" alt="Screenshot: Appearance preferences" width="672" height="735" style="width:100%; height:auto;" />](public/demo/preferences-appearance.jpg) | [<img loading="lazy" src="public/demo/preferences-appearance-details.jpg" alt="Screenshot: Appearance preferences (details)" width="672" height="934" style="width:100%; height:auto;" />](public/demo/preferences-appearance-details.jpg) |
|
||||
| [<img loading="lazy" src="public/demo/preferences-blacklist.jpg" alt="Screenshot: Blacklist preferences" width="672" height="655" style="width:100%; height:auto;" />](public/demo/preferences-blacklist.jpg) | [<img loading="lazy" src="public/demo/preferences-policies.jpg" alt="Screenshot: Policies preferences" width="672" height="353" style="width:100%; height:auto;" />](public/demo/preferences-policies.jpg) |
|
||||
|
||||
## CLI usage
|
||||
|
||||
You can run CLI commands directly on the executable inside the .app bundle (e.g. `/Applications/AltTab.app/Contents/MacOS/AltTab`)
|
||||
|
||||
* `AltTab --list` will return the list of windows (ID and title) in JSON
|
||||
* `AltTab --detailed-list` will return the list of windows (lots of info) in JSON
|
||||
* `AltTab --show=shortcut_index` will show AltTab UI, as if the corresponding shortcut was pressed
|
||||
* `AltTab --focus=window_id` will focus the window with the specified ID (get it from --list)
|
||||
* `AltTab --focusUsingLastFocusOrder=focus_order` will focus the window with the specified focus-order (get it from --detailed-list)
|
||||
|
||||
For this to work, you need AltTab.app to be running, to keep track of the state of windows, and execute the commands
|
||||
|
||||
## Alternatives
|
||||
|
||||
@@ -46,19 +71,13 @@ Before building my own app, I looked around at similar apps. However, none was c
|
||||
| [HyperSwitch](https://bahoom.com/hyperswitch) and [HyperDock](https://bahoom.com/hyperdock) | $10. Closed-source. Thumbnails are too small. App icon is on top of the thumbnail |
|
||||
| [WindowSwitcher](https://www.noteifyapp.com/windowswitcher/) | $7. Closed-source. Thumbnails are small and blurry. App icon is on top of the thumbnail |
|
||||
| [Switch](https://github.com/numist/Switch) | Open Source. Thumbnails are small. Very little customization. Latest release is from 2016 |
|
||||
| [Witch](https://manytricks.com/witch/) and [Context](https://contexts.co/) | $10-15. Closed-source. Focus on text. No thumbnails |
|
||||
| [Witch](https://manytricks.com/witch/) and [Contexts](https://contexts.co/) | $10-15. Closed-source. Focus on text. No thumbnails |
|
||||
| [MissionControl Plus](https://www.fadel.io/missioncontrolplus) | $10. Closed-source. No chronology and order to windows. Hard to navigate windows with keyboard |
|
||||
| Built-in [MissionControl](https://en.wikipedia.org/wiki/Mission_Control_(macOS)) | No keyboard support |
|
||||
| Built-in [MissionControl](https://en.wikipedia.org/wiki/Mission_Control_(macOS)) | No keyboard support |
|
||||
| Built-in `⌘ command` + `⇥ tab` | Only shows apps, not windows (note: can press down to see window of selected app) |
|
||||
| Built-in `⌘ command` + `` ` `` | Cycles through tabs and windows, but only of the same app. Only cycling, no direct access |
|
||||
|
||||
There are also related apps which don't really overlap in functionality, but target similar needs: [Swish](https://highlyopinionated.co/swish/), [Hookshot](https://hookshot.app/), [Magnet](https://magnet.crowdcafe.com/), [Spectacle](https://www.spectacleapp.com/), [Rectangle](https://github.com/rxhanson/Rectangle), [yabai](https://github.com/koekeishiya/yabai), [LayAuto](https://layautoapp.com/), [OptimalLayout](http://most-advantageous.com/optimal-layout/), [BetterTouchTool](https://folivora.ai/), [BetterSnapTool](https://folivora.ai/bettersnaptool), [Moom](https://manytricks.com/moom/), [uBar](https://brawersoftware.com/products/ubar)
|
||||
|
||||
## Screenshots
|
||||
|
||||
| 1 row | 2 rows | Windows theme |
|
||||
|-------|---------|-------|
|
||||
| [](public/demo/1-row.jpg) | [](public/demo/2-rows.jpg) | [](public/demo/windows-theme.jpg) |
|
||||
There are also related apps which don’t really overlap in functionality, but target similar needs: [Swish](https://highlyopinionated.co/swish/), [Hookshot](https://hookshot.app/), [Magnet](https://magnet.crowdcafe.com/), [Spectacle](https://www.spectacleapp.com/), [Rectangle](https://github.com/rxhanson/Rectangle), [yabai](https://github.com/koekeishiya/yabai), [LayAuto](https://layautoapp.com/), [BetterTouchTool](https://folivora.ai/), [BetterSnapTool](https://folivora.ai/bettersnaptool), [Moom](https://manytricks.com/moom/), [uBar](https://brawersoftware.com/products/ubar).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Support this project ❤️
|
||||
|
||||
<div class="support-buttons">
|
||||
<div class="support-button-wrapper">
|
||||
<a href="https://www.paypal.com/donate/?hosted_button_id=ACYAC3UYCM7CN" target="_blank" class="support-button">
|
||||
<img src="public/support/logo-paypal.png" class="paypal" alt="PayPal">
|
||||
</a>
|
||||
<img src="public/support/mascot-cat.png" class="mascot cat" alt="">
|
||||
</div>
|
||||
<div class="support-button-wrapper">
|
||||
<a href="https://github.com/sponsors/lwouis" target="_blank" class="support-button">
|
||||
<img src="public/support/logo-github-sponsors.png" class="github-sponsors" alt="GitHub Sponsors">
|
||||
</a>
|
||||
<img src="public/support/mascot-dog.png" class="mascot dog" alt="">
|
||||
</div>
|
||||
<div class="support-button-wrapper">
|
||||
<a href="https://www.patreon.com/alt_tab_macos" target="_blank" class="support-button">
|
||||
<img src="public/support/logo-patreon.svg" class="patreon" alt="Patreon">
|
||||
</a>
|
||||
<img src="public/support/mascot-squirrel.png" class="mascot squirrel" alt="">
|
||||
</div>
|
||||
<div class="support-button-wrapper">
|
||||
<a href="https://donate.stripe.com/4gw9D7bA3g3O0ikaEF" target="_blank" class="support-button">
|
||||
<img src="public/support/logo-stripe.svg" class="stripe" alt="Stripe">
|
||||
</a>
|
||||
<img src="public/support/mascot-rabbit.png" class="mascot rabbit" alt="">
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,6 +78,7 @@ nav {
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
color: #468ed8;
|
||||
margin-right: 5px;
|
||||
|
||||
span {
|
||||
padding-bottom: 1px;
|
||||
@@ -96,12 +97,28 @@ nav {
|
||||
}
|
||||
}
|
||||
|
||||
a.support {
|
||||
color: #c10029;
|
||||
|
||||
span {
|
||||
border-bottom: #c10029 solid 1px;
|
||||
}
|
||||
|
||||
&:hover, &.active {
|
||||
background: #ffd5ce;
|
||||
color: #c10029;
|
||||
|
||||
span {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.rainbow {
|
||||
background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
|
||||
animation: animatedgradient 5s linear alternate infinite;
|
||||
background-size: 300% 300%;
|
||||
text-decoration: none;
|
||||
margin-right: 10px;
|
||||
color: #fff;
|
||||
//box-shadow: #222 0px 1px;
|
||||
|
||||
@@ -139,7 +156,8 @@ nav {
|
||||
word-break: break-word;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
code, pre, tt {
|
||||
@@ -217,4 +235,109 @@ nav {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button row */
|
||||
.support-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
column-gap: 40px;
|
||||
row-gap: 55px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 70px;
|
||||
|
||||
/* Wrapper: each button keeps its mascot */
|
||||
.support-button-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px; /* ensures space for mascot below */
|
||||
}
|
||||
|
||||
/* Button shape + style */
|
||||
.support-button {
|
||||
width: 130px;
|
||||
height: 50px;
|
||||
border-radius: 22px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
background: linear-gradient(145deg, #ffffff, #dce8ff);
|
||||
box-shadow:
|
||||
4px 4px 10px rgba(0,0,0,0.15),
|
||||
-4px -4px 10px rgba(255,255,255,0.7);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Shine streak */
|
||||
.support-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -150%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(255,255,255,0) 0%,
|
||||
rgba(255,255,255,0.55) 50%,
|
||||
rgba(255,255,255,0) 100%
|
||||
);
|
||||
transform: skewX(-20deg);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.support-button:hover::before {
|
||||
animation: shine-sweep 0.8s forwards;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes shine-sweep {
|
||||
0% { left: -150%; }
|
||||
100% { left: 200%; }
|
||||
}
|
||||
|
||||
/* Button image */
|
||||
.support-button img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
/* Hover lift */
|
||||
.support-button:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow:
|
||||
6px 6px 15px rgba(0,0,0,0.18),
|
||||
-6px -6px 15px rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
/* Mascots */
|
||||
.mascot {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
transition: transform 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rabbit { width: 50px } /* long ears '^^ /*
|
||||
|
||||
/* Hover reactions */
|
||||
.support-button-wrapper:hover .cat { transform: translateX(-50%) translateY(-20px) rotate(-15deg); }
|
||||
.support-button-wrapper:hover .dog { transform: translateX(-50%) translateY(-20px) scaleX(-1); }
|
||||
.support-button-wrapper:hover .squirrel { transform: translateX(-50%) translateY(-20px) rotate(15deg); }
|
||||
.support-button-wrapper:hover .rabbit { transform: translateX(-50%) translateY(-20px) rotate(-10deg); }
|
||||
|
||||
.support-button img.paypal { width: 60%; margin-top: 6px; }
|
||||
.support-button img.github-sponsors { width: 110%; margin-top: 6px; }
|
||||
.support-button img.stripe { width: 51%; margin-top: 4px; }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Windows alt-tab on macOS">
|
||||
|
||||
<title>
|
||||
{% if page.title == "Introduction" %} {{ site.title }} - {{ site.description }}
|
||||
@@ -37,8 +38,7 @@
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<a href="{{ site.baseurl }}"><img src="{{ site.baseurl }}public/android-chrome-256x256.png" width="100"
|
||||
height="100"/></a>
|
||||
<a href="{{ site.baseurl }}" aria-label="Homepage"><img src="{{ site.baseurl }}public/android-chrome-256x256.png" width="100" height="100" alt="AltTab logo"/></a>
|
||||
<div>
|
||||
<h1><a href="{{ site.baseurl }}">{{ site.title }}</a></h1>
|
||||
<p>{{ site.github.project_tagline }}</p>
|
||||
@@ -48,15 +48,12 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class="rainbow" href="{{ site.github.latest_release.assets[0].browser_download_url }}">Download</a></li>
|
||||
<li><a class="{% if page.name == 'Introduction.md' %}active{% endif %}" href="{{ site.baseurl }}"><span>Introduction</span></a>
|
||||
</li>
|
||||
<li><a class="{% if page.name == 'Changelog.md' %}active{% endif %}" href="{{ site.baseurl }}changelog"><span>Release notes</span></a>
|
||||
</li>
|
||||
<li><a class="{% if page.name == 'Contributing.md' %}active{% endif %}"
|
||||
href="{{ site.baseurl }}contributing"><span>Contributing</span></a></li>
|
||||
<li><a class="{% if page.name == 'Thanks.md' %}active{% endif %}"
|
||||
href="{{ site.baseurl }}thanks"><span>Thanks</span></a></li>
|
||||
<li><a href="{{ site.github.repository_url }}"><span>GitHub project</span></a></li>
|
||||
<li><a class="{% if page.name == 'Introduction.md' %}active{% endif %}" href="{{ site.baseurl }}"><span>Introduction</span></a></li>
|
||||
<li><a class="{% if page.name == 'Changelog.md' %}active{% endif %}" href="{{ site.baseurl }}changelog"><span>Release notes</span></a></li>
|
||||
<li><a class="{% if page.name == 'Contributing.md' %}active{% endif %}" href="{{ site.baseurl }}contributing"><span>Contributing</span></a></li>
|
||||
<li><a class="{% if page.name == 'Thanks.md' %}active{% endif %}" href="{{ site.baseurl }}thanks"><span>Thanks</span></a></li>
|
||||
<li><a href="{{ site.github.repository_url }}"><span>GitHub</span></a></li>
|
||||
<li><a class="support {% if page.name == 'Support.md' %}active{% endif %}" href="{{ site.baseurl }}support"><span>Support this project ❤️</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 542 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 868 KiB After Width: | Height: | Size: 507 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="64" fill="none" viewBox="0 0 298 64">
|
||||
<defs>
|
||||
<linearGradient id="a" x1=".850001" x2="62.62" y1="62.72" y2="1.81" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9419"/>
|
||||
<stop offset=".43" stop-color="#FF021D"/>
|
||||
<stop offset=".99" stop-color="#E600FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#000" d="M86.4844 40.5858c0 .8464-.1792 1.5933-.5377 2.2505-.3585.6573-.8564 1.1651-1.5137 1.5236-.6572.3585-1.3941.5378-2.2406.5378H78v6.1044h5.0787c1.912 0 3.6248-.4282 5.1484-1.2846 1.5236-.8564 2.7186-2.0415 3.585-3.5452.8663-1.5037 1.3045-3.1966 1.3045-5.0886V21.0178h-6.6322v19.568Zm17.8556-1.8224h13.891v-5.6065H104.34v-6.3633h15.355v-5.7758H97.8766v29.9743h22.2464v-5.7757H104.34v-6.453Zm17.865-11.8005h8.882v24.0193h6.633V26.9629h8.842v-5.9451h-24.367v5.9551l.01-.01Zm47.022 9.0022c-.517-.2788-1.085-.4879-1.673-.6472.449-.1295.877-.2888 1.275-.488 1.096-.5676 1.962-1.3643 2.579-2.39.618-1.0257.936-2.2007.936-3.5351 0-1.5237-.418-2.8879-1.244-4.0929-.827-1.195-1.992-2.131-3.486-2.8082-1.494-.6672-3.206-1.0058-5.118-1.0058h-13.315v29.9743h13.574c2.011 0 3.804-.3485 5.387-1.0556 1.573-.707 2.798-1.6829 3.675-2.9476.866-1.2547 1.304-2.6887 1.304-4.302 0-1.4837-.338-2.8082-1.026-3.9833-.687-1.175-1.633-2.0812-2.858-2.7285l-.01.0099Zm-13.603-9.9184h5.886c.816 0 1.533.1494 2.161.4382.627.2888 1.115.707 1.464 1.2547.348.5378.527 1.1751.527 1.9021 0 .7269-.179 1.414-.527 1.9817-.349.5676-.837.9958-1.464 1.3045-.628.3087-1.345.4581-2.161.4581h-5.886v-7.3492.0099Zm10.138 18.134c-.378.5676-.916 1.0058-1.603 1.3145-.697.3087-1.484.4581-2.39.4581h-6.145v-7.6878h6.145c.886 0 1.673.1693 2.37.4979.687.3286 1.235.7867 1.613 1.3842.378.5975.578 1.2747.578 2.0414 0 .7668-.19 1.4241-.568 1.9917Zm29.596-5.3077c1.663-.7967 2.947-1.922 3.864-3.3659.916-1.444 1.374-3.117 1.374-5.0289 0-1.912-.448-3.5253-1.344-4.9592-.897-1.434-2.171-2.5394-3.814-3.3261-1.644-.7867-3.546-1.1751-5.717-1.1751h-13.124v29.9743h6.642V40.0779h4.322l6.084 10.9142h7.578l-6.851-11.7208c.339-.1195.677-.249.996-.3983h-.01Zm-2.151-6.1244c-.369.6274-.896 1.1154-1.583 1.444-.688.3386-1.494.5079-2.42.5079h-5.975v-8.2953h5.975c.926 0 1.732.1693 2.42.4979.687.3287 1.214.8166 1.583 1.434.368.6174.558 1.3544.558 2.1908 0 .8365-.19 1.5734-.558 2.2008v.0199Zm20.594-11.7308-10.706 29.9743h6.742l2.121-6.6122h11.114l2.27 6.6122h6.612L220.99 21.0178h-7.189Zm-.339 18.3431 3.445-10.5756.409-1.922.408 1.922 3.685 10.5756h-7.947Zm20.693 11.6312h6.851V21.0178h-6.851v29.9743Zm31.02-9.6993-12.896-20.275h-6.463v29.9743h6.055V30.7172l12.826 20.2749h6.533V21.0178h-6.055v20.275Zm31.528-3.3559c-.647-1.2448-1.564-2.2904-2.729-3.1369-1.165-.8464-2.509-1.4041-4.023-1.6929l-5.098-1.0456c-.797-.1892-1.434-.5178-1.902-.9958-.469-.478-.708-1.0755-.708-1.7825 0-.6473.17-1.205.518-1.683.339-.478.827-.8464 1.444-1.1153.618-.2689 1.335-.3983 2.151-.3983.817 0 1.554.1394 2.181.4182.627.2788 1.115.6672 1.464 1.1751s.528 1.0755.528 1.7228h6.642c-.04-1.7427-.528-3.2863-1.444-4.6207-.916-1.3443-2.201-2.3899-3.834-3.1468-1.633-.7568-3.505-1.1352-5.597-1.1352-2.091 0-3.943.3884-5.566 1.1751-1.623.7867-2.898 1.8721-3.804 3.2663-.906 1.3941-1.364 2.9775-1.364 4.76 0 1.444.288 2.7485.876 3.9036.587 1.1652 1.414 2.1311 2.479 2.8979 1.076.7668 2.311 1.3045 3.725 1.6033l5.397 1.1153c.886.2091 1.584.5975 2.101 1.1551.518.5577.767 1.2448.767 2.0813 0 .6672-.189 1.2747-.567 1.8025-.379.5277-.907.936-1.584 1.2248-.677.2888-1.474.4282-2.39.4282-.916 0-1.782-.1593-2.529-.478-.747-.3186-1.325-.7767-1.733-1.3742-.418-.5875-.617-1.2747-.617-2.0414h-6.642c.029 1.8721.527 3.5152 1.513 4.9492.976 1.424 2.32 2.5394 4.033 3.336 1.713.7967 3.675 1.195 5.886 1.195 2.21 0 4.202-.4083 5.915-1.2249 1.723-.8165 3.057-1.9418 4.023-3.3758.966-1.434 1.444-3.0572 1.444-4.8696 0-1.4838-.329-2.848-.976-4.1028l.02.01Z"/>
|
||||
<path fill="url(#a)" d="M20.34 3.66 3.66 20.34C1.32 22.68 0 25.86 0 29.18V59c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84V5c0-2.76-2.24-5-5-5H29.18c-3.32 0-6.49 1.32-8.84 3.66Z"/>
|
||||
<path fill="#000" d="M48 16H8v40h40V16Z"/>
|
||||
<path fill="#fff" d="M30 47H13v4h17v-4Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 88 KiB |