Compare commits

...

9 Commits

17 changed files with 810 additions and 75 deletions

349
package-lock.json generated
View File

@ -10,10 +10,13 @@
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.21", "@sveltejs/adapter-static": "^1.0.0-next.21",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"hast-util-to-text": "^3.1.2",
"mdsvex": "^0.9.8", "mdsvex": "^0.9.8",
"node-sass": "^6.0.1", "node-sass": "^6.0.1",
"svelte": "^3.42.6", "svelte": "^3.42.6",
"svelte-preprocess": "^4.9.8" "svelte-preprocess": "^4.9.8",
"unist-util-find": "^2.0.0",
"unist-util-visit": "^5.0.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -159,6 +162,15 @@
} }
} }
}, },
"node_modules/@types/hast": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz",
"integrity": "sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg==",
"dev": true,
"dependencies": {
"@types/unist": "^2"
}
},
"node_modules/@types/minimist": { "node_modules/@types/minimist": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@ -1433,6 +1445,36 @@
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true "dev": true
}, },
"node_modules/hast-util-is-element": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz",
"integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==",
"dev": true,
"dependencies": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-text": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
"integrity": "sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==",
"dev": true,
"dependencies": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0",
"hast-util-is-element": "^2.0.0",
"unist-util-find-after": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hosted-git-info": { "node_modules/hosted-git-info": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@ -1689,6 +1731,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"node_modules/lodash.iteratee": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz",
"integrity": "sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==",
"dev": true
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -2944,6 +2992,109 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/unist-util-find": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-find/-/unist-util-find-2.0.0.tgz",
"integrity": "sha512-I4NH8M2r7B9pGNXl41fftwCCi/WhG50RUdZDYcpEmiQEgHUYtfWpcCX4/R6s4XtK0ztxUSwC6u9sztlnDQAoaA==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0",
"lodash.iteratee": "^4.0.0",
"unist-util-visit": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find-after": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz",
"integrity": "sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find-after/node_modules/unist-util-is": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
"integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find/node_modules/unist-util-is": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
"integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find/node_modules/unist-util-visit": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz",
"integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^5.1.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find/node_modules/unist-util-visit-parents": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz",
"integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
"dev": true,
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"dev": true,
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-is/node_modules/@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
},
"node_modules/unist-util-stringify-position": { "node_modules/unist-util-stringify-position": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
@ -2957,6 +3108,47 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"dev": true,
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"dev": true,
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents/node_modules/@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
},
"node_modules/unist-util-visit/node_modules/@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
},
"node_modules/uri-js": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@ -3412,6 +3604,15 @@
"svelte-hmr": "^0.14.11" "svelte-hmr": "^0.14.11"
} }
}, },
"@types/hast": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz",
"integrity": "sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg==",
"dev": true,
"requires": {
"@types/unist": "^2"
}
},
"@types/minimist": { "@types/minimist": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@ -4312,6 +4513,28 @@
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true "dev": true
}, },
"hast-util-is-element": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz",
"integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==",
"dev": true,
"requires": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0"
}
},
"hast-util-to-text": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
"integrity": "sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==",
"dev": true,
"requires": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0",
"hast-util-is-element": "^2.0.0",
"unist-util-find-after": "^4.0.0"
}
},
"hosted-git-info": { "hosted-git-info": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@ -4525,6 +4748,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"lodash.iteratee": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz",
"integrity": "sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==",
"dev": true
},
"lru-cache": { "lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -5452,6 +5681,87 @@
"integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
"dev": true "dev": true
}, },
"unist-util-find": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-find/-/unist-util-find-2.0.0.tgz",
"integrity": "sha512-I4NH8M2r7B9pGNXl41fftwCCi/WhG50RUdZDYcpEmiQEgHUYtfWpcCX4/R6s4XtK0ztxUSwC6u9sztlnDQAoaA==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0",
"lodash.iteratee": "^4.0.0",
"unist-util-visit": "^4.0.0"
},
"dependencies": {
"unist-util-is": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
"integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0"
}
},
"unist-util-visit": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz",
"integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^5.1.1"
}
},
"unist-util-visit-parents": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz",
"integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
}
}
}
},
"unist-util-find-after": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz",
"integrity": "sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
},
"dependencies": {
"unist-util-is": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
"integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==",
"dev": true,
"requires": {
"@types/unist": "^2.0.0"
}
}
}
},
"unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0"
},
"dependencies": {
"@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
}
}
},
"unist-util-stringify-position": { "unist-util-stringify-position": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
@ -5461,6 +5771,43 @@
"@types/unist": "^2.0.2" "@types/unist": "^2.0.2"
} }
}, },
"unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"dependencies": {
"@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
}
}
},
"unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"dev": true,
"requires": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"dependencies": {
"@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==",
"dev": true
}
}
},
"uri-js": { "uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -12,7 +12,10 @@
"mdsvex": "^0.9.8", "mdsvex": "^0.9.8",
"node-sass": "^6.0.1", "node-sass": "^6.0.1",
"svelte": "^3.42.6", "svelte": "^3.42.6",
"svelte-preprocess": "^4.9.8" "svelte-preprocess": "^4.9.8",
"unist-util-visit": "^5.0.0",
"unist-util-find": "^2.0.0",
"hast-util-to-text": "^3.1.2"
}, },
"type": "module" "type": "module"
} }

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" /> <link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2" />
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -20,9 +20,9 @@
text-transform: uppercase; text-transform: uppercase;
color: #8c0606; color: #8c0606;
/* box-sizing: border-box;*/ /* box-sizing: border-box;*/
font-size: calc(var(--content-size) * var(--content-line-height) * 1.9); font-size: calc(var(--content-size) * var(--content-line-height) * 1.75);
float: left; float: left;
font-family: serif; font-family: 'Baskerville';
line-height: 0.8; line-height: 0.8;
margin-right: 0.1em; margin-right: 0.1em;
display: block; display: block;
@ -30,12 +30,12 @@
.first-word { .first-word {
margin-left: var(--shift); margin-left: var(--shift);
font-variant: petite-caps;
} }
</style> </style>
<p>
<span class="drop-cap">{initial}</span>
<span class="first-word" style:--shift={shift}>{remainder}</span>
<slot></slot>
</p>
<span class="drop-cap">{initial}</span>
{#if remainder.length}
<span class="first-word" style:--shift={shift}>{remainder}</span>
{/if}

52
src/lib/Heading.svelte Normal file
View File

@ -0,0 +1,52 @@
<script>
export let level;
export let id = '';
const tag = `h${level}`;
</script>
<style>
a {
/* Works better to set the size here for line-height reasons */
font-size: 0.9em;
color: hsl(0, 0%, 50%);
}
a:hover {
border-bottom: 0.05em solid currentcolor;
}
svg {
width: 1em;
}
.before {
display: none;
margin-right: 0.5rem;
margin-left: calc(-1em - 0.5rem);
}
@media(min-width: 58rem) {
.before {
display: inline;
}
.after {
display: none;
}
}
</style>
<svelte:element this={tag} {id} class="h">
<a href="#{id}" class="before">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg></a><span> <!-- Looks ugly but necessary to get rid of spurious whitespace -->
<slot></slot>
</span>
<!-- Icon from https://heroicons.com/ -->
<a href="#{id}" class="after">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</a>
</svelte:element>

View File

@ -1,7 +1,7 @@
<script context="module"> <script context="module">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { formatDate } from './datefmt.js'; import { formatDate } from './datefmt.js';
import { makeSlug } from '$lib/slug.js'; import { makeSlug } from '$lib/utils.js';
import Link from './Link.svelte'; import Link from './Link.svelte';
export { Link as a }; export { Link as a };
@ -17,6 +17,7 @@
.subtitle { .subtitle {
font-size: 0.9em; font-size: 0.9em;
font-style: italic; font-style: italic;
margin-top: -0.5rem;
} }
</style> </style>

View File

@ -10,7 +10,7 @@
margin-left: 0.05rem; margin-left: 0.05rem;
&:after { &:after {
font-size: 0.75rem; font-size: 0.75em;
position: relative; position: relative;
bottom: 0.3rem; bottom: 0.3rem;
color: #8c0606; color: #8c0606;
@ -23,9 +23,13 @@
&:before { &:before {
content: counter(sidenote) " "; content: counter(sidenote) " ";
position: relative; /* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
(because the sidenote is floated it counts as a positioned parent, I think) */
position: absolute;
/* translate moves it out to the left (and just a touch up to mimic the superscript efect)
-100% refers to the width of the element, so it pushes it out further if necessary (i.e. two digits instead of one) */
transform: translate(calc(-100% - 0.2rem), -0.15rem);
font-size: 0.75rem; font-size: 0.75rem;
bottom: 0.2rem;
color: #8c0606; color: #8c0606;
} }
} }
@ -43,7 +47,7 @@
.sidenote { .sidenote {
--gap: 2rem; --gap: 2rem;
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2)); --sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
max-width: var(--sidenote-width); width: var(--sidenote-width);
hyphens: auto; hyphens: auto;
position: relative; position: relative;
float: right; float: right;
@ -52,6 +56,12 @@
margin-bottom: 0.7rem; margin-bottom: 0.7rem;
} }
.nested.sidenote {
margin-right: 0;
margin-top: 0.7rem;
margin-bottom: 0;
}
.dismiss { .dismiss {
display: none; display: none;
} }
@ -124,6 +134,21 @@
</script> </script>
<script> <script>
import { onMount } from 'svelte';
let noteBody;
let nested = false;
onMount(() => {
// check to see if the parent node is also a sidenote, if so move this one to the end
let parentNote = noteBody.parentElement.closest('span.sidenote');
if (parentNote) {
noteBody.remove();
parentNote.appendChild(noteBody);
nested = true;
}
});
const id = Math.random().toString().slice(2); const id = Math.random().toString().slice(2);
let toggle; let toggle;
@ -143,7 +168,7 @@
<label for={id} on:click={toggleState} class="counter"></label> <label for={id} on:click={toggleState} class="counter"></label>
<input {id} bind:this={toggle} type="checkbox" class="sidenote-toggle" /> <input {id} bind:this={toggle} type="checkbox" class="sidenote-toggle" />
<span class="sidenote"> <span class="sidenote" class:nested bind:this={noteBody}>
<label class="dismiss" for={id} on:click={toggleState}>&times;</label> <label class="dismiss" for={id} on:click={toggleState}>&times;</label>
<slot></slot> <slot></slot>
</span> </span>

View File

@ -0,0 +1,48 @@
<script>
export let floatingCounter = true;
export let classes = '';
export {classes as class};
</script>
<style>
:global(body) {
counter-reset: sidenote unstyled-sidenote;
}
.counter {
counter-increment: unstyled-sidenote;
margin-left: 0.05rem;
}
.counter::after {
content: counter(unstyled-sidenote);
font-size: 0.75em;
position: relative;
bottom: 0.3em;
color: #0083c4;
}
.sidenote {
color: var(--content-color-faded);
font-size: 0.8rem;
}
.sidenote.floatingCounter::before {
content: counter(unstyled-sidenote);
font-size: 0.75rem;
color: #0083c4;
/* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */
position: absolute;
/* ...except that we move it out to the left and up a bit, so
it's hanging out in space. 100% refers to the width of this
pseudo-element, so we handle different-sized counters the same. */
transform: translate(
calc(-100% - 0.16em),
-0.12em
);
}
</style>
<span class="counter"></span>
<span class="sidenote {classes}" class:floatingCounter={floatingCounter}>
<slot></slot>
</span>

View File

@ -1,50 +0,0 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s/g
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '')
}
function apply(node, types, fn) {
if (typeof types === 'string') {
types = new Set([types]);
}
else if (!(types instanceof Set)) {
types = new Set(types)
console.log(types)
}
if (types.has(node.type)) {
fn(node);
}
if ('children' in node) {
for (let child of node.children) {
apply(child, types, fn);
}
}
}
function getTextContent(node) {
let segments = [];
apply(node, 'text', textNode => {
// skip all-whitespace strings
if (textNode.value.match(/^\s+$/)) return;
segments.push(textNode.value.trim());
});
return segments.join(' ');
}
export default function slug() {
return (tree) => {
apply(tree, 'element', e => {
if (e.tagName.match(/h[1-6]/)) {
let text = getTextContent(e);
e.properties.id = makeSlug(text);
}
})
}
}

8
src/lib/utils.js Normal file
View File

@ -0,0 +1,8 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s+/g;
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '');
}

90
src/plugins/rehype.js Normal file
View File

@ -0,0 +1,90 @@
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
import { find } from 'unist-util-find';
import { toText } from 'hast-util-to-text';
import { makeSlug } from '../lib/utils.js';
export function localPlugins() {
let printed = false;
return (tree, vfile) => {
const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false;
let moduleScript;
let imports = new Set();
if (needsDropcap) {
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
}
visit(tree, node => {
// add slugs to headings
if (isHeading(node)) {
processHeading(node);
imports.add("import Heading from '$lib/Heading.svelte';");
return SKIP;
}
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
if (isModuleScript(node)) {
moduleScript = node;
}
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node);
dropcapAdded = true;
return SKIP;
}
});
// insert our imports at the top of the `<script context="module">` tag
if (imports.size > 0) {
const script = moduleScript.value;
// split the script where the opening tag ends
const i = script.indexOf('>');
const openingTag = script.slice(0, i + 1);
const remainder = script.slice(i + 1);
// mdvsex uses tabs so we will as well
const importScript = Array.from(imports).join('\n\t');
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
}
}
}
function processHeading(node) {
const level = node.tagName.slice(1);
node.tagName = 'Heading';
node.properties.level = level;
node.properties.id = makeSlug(toText(node));
}
function addDropcap(par) {
let txtNode = find(par, {type: 'text'});
const i = txtNode.value.search(/\s/);
const firstWord = txtNode.value.slice(0, i);
const remainder = txtNode.value.slice(i);
par.children.unshift({
type: 'raw',
value: `<Dropcap word="${firstWord}" />`,
});
txtNode.value = remainder;
}
function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/);
}
function isModuleScript(node) {
return node.type === 'raw' && node.value.match(/^<script context="module">/);
}
function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p';
}

View File

@ -3,6 +3,7 @@ title: Imagining A Passwordless Future
description: Can we replace passwords with something more user-friendly? description: Can we replace passwords with something more user-friendly?
date: 2021-04-30 date: 2021-04-30
draft: true draft: true
dropcap: false
--- ---
<script> <script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '$lib/Sidenote.svelte';

View File

@ -0,0 +1,199 @@
---
title: Sidenotes
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
date: 2023-08-14
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
import UnstyledSidenote from '$lib/UnstyledSidenote.svelte';
</script>
<style>
.counter {
counter-increment: sidenote;
}
.counter::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-left: 0.1rem;
}
.sidenote-absolute {
position: absolute;
left: calc(50% + min(100%, var(--content-width)) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
.sidenote-absolute::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-right: 0.1rem;
}
:global(.sn-float) {
float: right;
}
:global(.sn-clear) {
float: right;
clear: right;
}
:global(.sn-gutter) {
float: right;
width: 14rem;
margin-right: -14rem;
}
:global(.sn-gap) {
float: right;
width: 14rem;
margin-right: -16rem;
}
:global(.sn-var-width) {
float: right;
--width: min(14rem, calc(50vw - var(--content-width) / 2) - 2rem);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
</style>
One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote>
Footnotes don't really work on the web the way they do on paper, since the web doesn't have page breaks. You _can_ stick your footnotes in a floating box at the bottom of the page, so they're visible at the bottom of the text just like they would be on a printed page, but this sacrifices precious vertical space.<Sidenote>On mobile, it's _horizontal_ space that's at a premium, so I do use this approach there. Although I'm a pretty heavy user of sidenotes, so I have to make them toggleable as well or they'd fill up the entire screen.</Sidenote> Plus, you usually end up with the notes further away from the point of divergence than they would be as sidenotes anyway.
I'm also not a huge fan of show-on-hover/click for marginalia, because it requires an extra interaction--and often a fairly precise one, which is always annoying.<Sidenote>This is especially true on mobile, where I've found myself selecting text instead of showing/hiding a note because I didn't get my finger in quite the right place.</Sidenote> Admittedly this style _does_ get you the absolute minimum distance between the marginalia and the main content, but I think the extra interaction is too heavy a price to pay.<Sidenote>Except on mobile, as mentioned. Mobile displays just don't have _any_ extra space at all, so you're left choosing between various unappealing options.</Sidenote>
So we're left with sidenotes, which I consider the crème de la crème of web-based marginalia. So okay, sure, sidenotes are great and all, but how do we actually _do_ them? Well! _wipes imaginary sweat from brow_ It sure was tough, and for a while there I thought I'd never make it through, but I done did figgered it out in the end!<Sidenote>_Narrator:_ He had not figured it out. He had googled around until he found someone else who had figured it out, and then copied their solution.</Sidenote>
## The Suboptimal Solution: Absolute Positioning
I'm naturally lazy, so I wanted the authoring experience to be as low-friction as possible so that I wouldn't end up foregoing sidenotes just because they were too much of a pain to put in. Since I had already settled on [mdsvex](https://mdsvex.pngwn.io/docs) for authoring my posts, I wanted sidenotes to be just another component that I could throw in mid-stream whenever I had an ancillary thought to express. This meant that DOM-wise, the sidenotes were going to be mixed right in with the main body text. Since I was also hoping to do this in pure CSS,<Sidenote>Because as much as I claim not to care, I secretly fear the Hacker News anti-Javascript brigade and desperately crave their approval.</Sidenote> meant that I was going to have to do something that removed the sidenote from the normal document flow, such as `position: absolute`.
My first approach was something like this:
```css
.sidenote {
position: absolute;
/* 50% takes us to the midpoint of the page,
half of content-width gets out out to the gutter,
and the extra 1rem gives us some breathing room. */
left: calc(50% + var(--content-width) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
```
And it worked! Sort of. Here's an example.<span class="counter"></span><span class="sidenote-absolute">My initial take on sidenotes. Seems to be working, right?</span> Unfortunately it has a major flaw: Absolute positioning removes an element from the document flow _entirely_, while I wanted sidenotes to still flow with _each other_, That doesn't happen with this solution--if you have multiple sidenotes too close together, they will overlap because absolute positioning Just Doesn't Care.<span class="counter"></span><span class="sidenote-absolute">Like this one.</span><span class="counter"><span class="sidenote-absolute" style="transform: translateY(0.2rem)">And this one, which I've moved down just a smidge to make the overlap more apparent.</span>
Obviously, it isn't that hard to just scan through the page looking for sidenotes, detect when they overlap, and then (since they're already absolutely positioned) adjust their `top` values appropriately to get rid of the overlap. But I didn't want to do this for a variety of reasons.
* I wanted to write this as a Svelte component, which means that's the obvious place to put this logic. But because there are many instances of the component and I only want to run the collision-detection logic once, it has to be coordinated across multiple instances of the same component, which is painful.
* Because we have to wait for the sidenote elements to _have_ concrete positions before we can detect whether they collide, we can't do this until they are mounted (i.e. inserted into the DOM). I was concerned that this would cause [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content)-like problems, although in retrospect I don't actually recall it happening.<Sidenote>Possibly it was mitigated by the way Svelte batches DOM updates.</Sidenote>However, since I was always planning on static-rendering the site and letting SvelteKit do client-side hydration on page load, I don't think the possibility could ever be ruled out entirely.
* Anything that triggered a reflow could cause the text to move around, but the sidenotes might not follow suit.<Sidenote>Specifically: sidenotes that had been adjusted to get rid of overlap would stay where they were, because they would already have an explicit `top` property. Sidenotes that hadn't been adjusted would move up and down as text reflowed, but this meant they could end up overlapping again.</Sidenote> [There are a lot of things that can cause a reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a),<Sidenote>And this is just the ones that come from Javascript! It doesn't even address stuff like resizing the window or expanding/collapsing a `<details>` element.</Sidenote> and I'd have to listen to all of them if I wanted this to be a fully general solution. Sure, I could just be aware of this problem and avoid using reflow-causing events where possible--but I wanted the freedom to be able to add as much interactivity as I felt like to any given blog post without having to worry.
None of these problems are _completely_ inaddressible, but it was all going to be very fiddly to fix properly, so I decided to do a bit more research before throwing in the towel. And boy am I glad that I did, because it turns out that with enough...
## CSS Wizardry
...anything is possible.
Eventually I ran across [this post](https://scripter.co/sidenotes-using-only-css/), which solved my problem almost perfectly. The basic idea is extremely straightforward:
1. Give your sidenotes a `float` and `clear` in the same direction, so that they are removed from the regular document flow _but_ (and this is crucual) _they will still take each other into account for layout purposes._
2. Give them a fixed width, and then:
3. Give them a negative margin equal to the max-width, so that they are pulled out of the body of the text and hang out in the gutter.
It's shockingly simple, to be honest--I would never have thought of it myself, but I'm glad somebody out there did.<Sidenote>It's worth noting that this same approach seems to be used by [Tufte CSS](https://edwardtufte.github.io/tufte-css/), which I had looked at previously but had failed to comprehend, possibly because it doesn't really go into detail about its sidenote mechanism.</Sidenote> The only problem is that you can't nest sidenotes, which is something I had hoped to support, but we'll get to that in a bit.
## Implementation
It took me quite a while (longer than it should have, probably) to really grok this, so I wanted to go through the implementation step-by-step and show the effect of each component part. For starters, let's just get the basic appearance out of the way:
```css
body {
counter-reset: sidenote;
}
.counter {
counter-increment: sidenote;
margin-left: 0.05rem;
}
.counter::after {
content: counter(unstyled-sidenote);
font-size: 0.75em;
position: relative;
bottom: 0.3em;
color: var(--accent-color);
}
.sidenote {
color: var(--content-color-faded);
font-size: 0.8rem;
}
.sidenote::before {
content: counter(unstyled-sidenote);
font-size: 0.75rem;
color: var(--accent-color);
/* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */
position: absolute;
/* ...except that we move it out to the left and up a bit, so
it's hanging out in space. 100% refers to the width of this
pseudo-element, so we handle different-sized counters the same. */
transform: translate(
calc(-100% - 0.16em),
-0.12em
);
}
```
This handles font size, color, and counters--CSS counters are very convenient for this, because they automatically adjust themselves whenever I go back and add or remove a sidenote earlier in the page. That gives us sidenote that looks like this:<UnstyledSidenote floatingCounter={false}>We're going to use a different color counter for these ones, so they can be more easily distinguished.</UnstyledSidenote>
It's still in flow, so our first change will be to remove it from the standard flow with `float: right`. Doing that moves it over to the side, like so.<UnstyledSidenote class="sn-float">The float also unmoors it from the text baseline.</UnstyledSidenote> Notice how it still takes up space in the body text, even though it's happening in a different place than its DOM location.
To keep it from doing that, we'll add a combination of a fixed width and a negative margin. The fixed width is primarily to give us a "target" number for the negative margin, since there isn't a good way to reference the width of the _current_ item when defining margins. (`margin-right: 100%` would move it by the width of the _containing_ block, which is not what we want.) With that in place, here's what we get.<UnstyledSidenote class="sn-gutter">Looking pretty good!</UnstyledSidenote> Unfortunately this example and subsequent ones don't work on mobile, since there are no gutters. Sorry about that! You'll have to view the desktop version to make them show up.
The next step is to keep the sidenotes from overlapping when there are multiple of them in quick succession, like these two.<UnstyledSidenote class="sn-gutter">This is one sidenote.</UnstyledSidenote><UnstyledSidenote class="sn-gutter">Another sidenote, which overlaps the first.</UnstyledSidenote> We do that with the `clear` property, which, when applied to a floated element, causes it to drop below any preceding floated elements on the specified side with which it would otherwise share a line.
This is easiest to show with an example, so let's do that. Here are two sidenotes with just `float: right` and no negative margin.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-float">Two.<span style="margin-right: 0.75rem"></span></UnstyledSidenote> [[Click here]] to animate the negative margin being applied to first the one, then the other. Applying negative margin to the first sidenote creates space for the other one to move to the side, since by nature floats want to form a horizontal row against the side of their containing block. Once we start applying negative margin to the second sidenote, though, normal flow rules don't apply, and they start to overlap.
This is fixed by `clear` because it changes the behavior of floats. Here are the same two sidenotes as above, but with `clear: right` applied to the second.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-clear">Two.</UnstyledSidenote> The `clear` property causes the second sidenote to drop below the first, which happens to be exactly the behavior that we want. All that's left is to apply the negative margin like so<UnstyledSidenote class="sn-clear sn-gutter">Three.</UnstyledSidenote><UnstyledSidenote class="sn-clear sn-gutter">Four.</UnstyledSidenote>and the whole stack will slide right over into the gutter.
It's smack up against the body text, though. In fact, since the floating counter hangs off to the left, it actually overlaps with the body text.(Depending on line wrapping, this may not be immediately apparent from the above.)
We can fix that in one of two ways. 1) We can increase the negative margin so that it's _greater_ than the width of the sidenote, or 2) We can just stick in some padding.<UnstyledSidenote class="sn-gap">Voila! Collision avoided.</UnstyledSidenote> I like the first option better, because it better reflects what we're actually doing here--margin is for creating caps _outside_ and _between_ elements, while padding is for gaps _inside_.
Here's what we have so far:
```css
.sidenote {
float: right;
width: 14rem;
margin-right: -16rem;
}
```
We still have a bit of a problem, though. Because we've assigned the sidenote a fixed width, it doesn't automatically shrink when the window gets too small for it. Obviously, of course, at _some_ point we're going to switch to the mobile version, which displays at the bottom of the screen and can be toggled on or off. But there are plenty of widths where sidenotes would still work perfectly well, just with a slightly narrower width than our initial `14rem`.
Fortunately, CSS `calc()` is widely supported and does exactly what we need.<UnstyledSidenote class="sn-var-width">Here we are! You may need to resize your window to get full effect.</UnstyledSidenote> Let's take a look:
```css
.sidenote {
float: right;
--width: min(
14rem,
calc( (100vw - var(--content-width) ) / 2 - 2rem )
);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
```
To calculate the width, we take the full viewport (`100vw`) and subtract the width of the main column (`var(--content-width)`). This gives us the combined width of both gutters, but since we only want the width of a single gutter we divide by 2. Then we subtract a further `2rem` so that our width is a little less than the full width of ther gutter, to give us some breathing room.
For the margin, we just take the value we calculated for the width and subtract it from 0 (to make it negative), then subtract a further 2rem to pull the sidenote out by that much more to give us breathing room.

View File

@ -2,21 +2,19 @@
title: Thoughts on Vue vs Svelte title: Thoughts on Vue vs Svelte
description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences. description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences.
date: 2023-06-29 date: 2023-06-29
draft: true
--- ---
<script> <script>
import Dropcap from '$lib/Dropcap.svelte';
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '$lib/Sidenote.svelte';
</script> </script>
<Dropcap word="Recently">I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.</Dropcap> Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.
Of course, this is only possible because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as: Of course, this is only possible because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
* Single-file components with separate sections for markup, style, and logic * Single-file components with separate sections for markup, style, and logic
* Automatically reactive data bindings * Automatically reactive data bindings
* Two-way data binding (a point of almost religious contention in certain circles) * Two-way data binding (a point of almost religious contention in certain circles)
* ...other things that I can't remember right now * An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template<Sidenote>Or single-file component, anyway.</Sidenote> embeds the logic, whereas in React, the logic embeds the template.
I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that's all I've used. I've only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I've never used it myself. I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that's all I've used. I've only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I've never used it myself.
@ -54,7 +52,7 @@ While Svelte takes the more common approach of wrapping bits of markup in its ow
</div> </div>
``` ```
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying about something so trivial, but hey, this whole post is one big bikeshed anyway.</Sidenote> While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying<Sidenote>Nested<Sidenote>Doubly-nested sidenote!</Sidenote> sidenote!</Sidenote> about something so trivial,<Sidenote>Second nested sidenote.</Sidenote> but hey, this whole post is one big bikeshed anyway.</Sidenote>
Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue's custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.<Sidenote>AlpineJS also works this way, but this is the *only* way that it works - it doesn't have an equivalent for Vue's full-fat "app mode" as it were.</Sidenote> This strikes me as a fantastic way to ease the transition between "oh I just need a tiny bit of interactivity on this page, so I'll just sprinkle in some inline components" and "whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now." Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue's custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.<Sidenote>AlpineJS also works this way, but this is the *only* way that it works - it doesn't have an equivalent for Vue's full-fat "app mode" as it were.</Sidenote> This strikes me as a fantastic way to ease the transition between "oh I just need a tiny bit of interactivity on this page, so I'll just sprinkle in some inline components" and "whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now."
@ -62,7 +60,7 @@ Detractors of this approach might point out that it's harder to spot things like
Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including `:` for `v-bind` and `@` for `v-on`. Svelte doesn't really have an equivalent for this, although it does allow you to shorten `attr={attr}` to `{attr}`, which can be convenient. Which might as well bring us to: Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including `:` for `v-bind` and `@` for `v-on`. Svelte doesn't really have an equivalent for this, although it does allow you to shorten `attr={attr}` to `{attr}`, which can be convenient. Which might as well bring us to:
## Data binding ## Data Binding
I give this one to Svelte overall, although Vue has a few nice conveniences going for it. I give this one to Svelte overall, although Vue has a few nice conveniences going for it.
@ -77,7 +75,7 @@ Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component
<ChildComponent v-model="childValue" />` <ChildComponent v-model="childValue" />`
``` ```
But _inside_ the child component: But _inside_ the child component:
```markup ```js
export default { export default {
props: ['modelValue'], props: ['modelValue'],
emits: ['update:modelValue'], emits: ['update:modelValue'],
@ -163,4 +161,4 @@ As far as bundle size goes, it's highly dependent on how many components you're
### Ecosystem ### Ecosystem
Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses [SvelteKit](https://kit.svelte.dev) and [MDSvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it's fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte's immaturity issues plus more of its own.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). All in all I'd say if a robust ecosystem is important to you then Vue is probably the better choice at this point. Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses [SvelteKit](https://kit.svelte.dev) and [mdsvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it's fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte's immaturity issues plus more of its own.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). All in all I'd say if a robust ecosystem is important to you then Vue is probably the better choice at this point.

Binary file not shown.

View File

@ -7,10 +7,20 @@
font-display: block; font-display: block;
} }
@font-face {
font-family: 'Baskerville';
font-style: normal;
font-weight: 400;
src: url(/Baskerville-Regular.woff2) format('woff2');
font-display: block;
}
:root { :root {
--content-size: 1.25rem; --content-size: 1.25rem;
--content-line-height: 1.3; --content-line-height: 1.3;
--content-color: #1e1e1e; --content-color: #1e1e1e;
--content-color-faded: #555;
--accent-color: #8c0606;
} }
html { html {

View File

@ -1,7 +1,9 @@
import { mdsvex } from 'mdsvex'; import { mdsvex } from 'mdsvex';
import staticAdapter from '@sveltejs/adapter-static'; import staticAdapter from '@sveltejs/adapter-static';
import svp from 'svelte-preprocess'; import svp from 'svelte-preprocess';
import slug from './src/lib/slug.js'; // import slug from './src/lib/slug.js';
// import { addDropcaps } from './src/lib/dropcapify.js';
import { localPlugins } from './src/plugins/rehype.js';
const config = { const config = {
@ -9,7 +11,7 @@ const config = {
preprocess: [ preprocess: [
mdsvex({ mdsvex({
layout: './src/lib/Post.svelte', layout: './src/lib/Post.svelte',
rehypePlugins: [slug], rehypePlugins: [localPlugins],
}), }),
svp.scss(), svp.scss(),
], ],