commit
12d72de654
|
@ -2,6 +2,8 @@ const eleventyNavigationPlugin = require("@11ty/eleventy-navigation");
|
||||||
const eleventySyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
const eleventySyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||||
const eleventySass = require("eleventy-sass");
|
const eleventySass = require("eleventy-sass");
|
||||||
const mdDefList = require("markdown-it-deflist");
|
const mdDefList = require("markdown-it-deflist");
|
||||||
|
const mdToc = require("markdown-it-table-of-contents");
|
||||||
|
const mdAnchor = require("markdown-it-anchor");
|
||||||
const eleventyRss = require("@11ty/eleventy-plugin-rss");
|
const eleventyRss = require("@11ty/eleventy-plugin-rss");
|
||||||
|
|
||||||
module.exports = function (eleventyConfig) {
|
module.exports = function (eleventyConfig) {
|
||||||
|
@ -24,7 +26,12 @@ module.exports = function (eleventyConfig) {
|
||||||
loadPaths: ["node_modules"],
|
loadPaths: ["node_modules"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
eleventyConfig.amendLibrary("md", mdLib => mdLib.use(mdDefList));
|
eleventyConfig.amendLibrary("md", mdLib => mdLib
|
||||||
|
.use(mdDefList)
|
||||||
|
.use(mdToc, {
|
||||||
|
includeLevel: [ 1, 2, 3, 4, 5, 6 ]
|
||||||
|
})
|
||||||
|
.use(mdAnchor));
|
||||||
|
|
||||||
eleventyConfig.addFilter("IsNotPage", (collection, url) =>
|
eleventyConfig.addFilter("IsNotPage", (collection, url) =>
|
||||||
collection.filter(item => item.url != url));
|
collection.filter(item => item.url != url));
|
||||||
|
|
96
package-lock.json
generated
96
package-lock.json
generated
|
@ -11,6 +11,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@11ty/eleventy-plugin-rss": "^1.2.0",
|
"@11ty/eleventy-plugin-rss": "^1.2.0",
|
||||||
"feather-icons": "^4.29.0",
|
"feather-icons": "^4.29.0",
|
||||||
|
"markdown-it-anchor": "^8.6.7",
|
||||||
|
"markdown-it-table-of-contents": "^0.6.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"prism-themes": "^1.9.0"
|
"prism-themes": "^1.9.0"
|
||||||
},
|
},
|
||||||
|
@ -298,6 +300,28 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "12.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||||
|
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "*",
|
||||||
|
"@types/mdurl": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/minimatch": {
|
"node_modules/@types/minimatch": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||||
|
@ -1411,7 +1435,6 @@
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uc.micro": "^1.0.1"
|
"uc.micro": "^1.0.1"
|
||||||
}
|
}
|
||||||
|
@ -1481,7 +1504,6 @@
|
||||||
"version": "13.0.1",
|
"version": "13.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
"entities": "~3.0.1",
|
"entities": "~3.0.1",
|
||||||
|
@ -1493,17 +1515,33 @@
|
||||||
"markdown-it": "bin/markdown-it.js"
|
"markdown-it": "bin/markdown-it.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it-anchor": {
|
||||||
|
"version": "8.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
|
||||||
|
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/markdown-it": "*",
|
||||||
|
"markdown-it": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/markdown-it-deflist": {
|
"node_modules/markdown-it-deflist": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
|
||||||
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==",
|
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it-table-of-contents": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">6.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/markdown-it/node_modules/argparse": {
|
"node_modules/markdown-it/node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/maximatch": {
|
"node_modules/maximatch": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
@ -1553,8 +1591,7 @@
|
||||||
"node_modules/mdurl": {
|
"node_modules/mdurl": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
|
@ -2353,8 +2390,7 @@
|
||||||
"node_modules/uc.micro": {
|
"node_modules/uc.micro": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
"version": "3.15.3",
|
"version": "3.15.3",
|
||||||
|
@ -2654,6 +2690,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/linkify-it": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"@types/markdown-it": {
|
||||||
|
"version": "12.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||||
|
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/linkify-it": "*",
|
||||||
|
"@types/mdurl": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/mdurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||||
|
@ -3479,7 +3537,6 @@
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||||
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"uc.micro": "^1.0.1"
|
"uc.micro": "^1.0.1"
|
||||||
}
|
}
|
||||||
|
@ -3531,7 +3588,6 @@
|
||||||
"version": "13.0.1",
|
"version": "13.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
|
||||||
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
"entities": "~3.0.1",
|
"entities": "~3.0.1",
|
||||||
|
@ -3543,17 +3599,27 @@
|
||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"markdown-it-anchor": {
|
||||||
|
"version": "8.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
|
||||||
|
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"markdown-it-deflist": {
|
"markdown-it-deflist": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
|
||||||
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==",
|
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"markdown-it-table-of-contents": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-jHvEjZVEibyW97zEYg19mZCIXO16lHbvRaPDkEuOfMPBmzlI7cYczMZLMfUvwkhdOVQpIxu3gx6mgaw46KsNsQ=="
|
||||||
|
},
|
||||||
"maximatch": {
|
"maximatch": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz",
|
||||||
|
@ -3592,8 +3658,7 @@
|
||||||
"mdurl": {
|
"mdurl": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
|
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"merge2": {
|
"merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
|
@ -4204,8 +4269,7 @@
|
||||||
"uc.micro": {
|
"uc.micro": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.15.3",
|
"version": "3.15.3",
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@11ty/eleventy-plugin-rss": "^1.2.0",
|
"@11ty/eleventy-plugin-rss": "^1.2.0",
|
||||||
"feather-icons": "^4.29.0",
|
"feather-icons": "^4.29.0",
|
||||||
|
"markdown-it-anchor": "^8.6.7",
|
||||||
|
"markdown-it-table-of-contents": "^0.6.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"prism-themes": "^1.9.0"
|
"prism-themes": "^1.9.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
border: solid 1px var(--primary-border-color);
|
border: solid 1px var(--primary-border-color);
|
||||||
border-radius: var(--main-border-radius);
|
border-radius: var(--main-border-radius);
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin-bottom: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-links {
|
.card-links {
|
||||||
|
|
|
@ -12,6 +12,7 @@ $code-background-color-dark: lighten(variables.$background-color-dark, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
font-size: 0.9em;
|
||||||
background: var(--code-background);
|
background: var(--code-background);
|
||||||
padding: 0.125em 0.25em;
|
padding: 0.125em 0.25em;
|
||||||
border: solid 1px var(--primary-border-color);
|
border: solid 1px var(--primary-border-color);
|
||||||
|
@ -19,6 +20,7 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
font-size: 0.9em;
|
||||||
background: var(--code-background);
|
background: var(--code-background);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border: solid 1px var(--primary-border-color);
|
border: solid 1px var(--primary-border-color);
|
||||||
|
@ -30,6 +32,11 @@ pre {
|
||||||
border: unset;
|
border: unset;
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[class*=language-] {
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.keyword {
|
.token.keyword {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-top: 0;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
10
src/css/Components/_table-of-contents.scss
Normal file
10
src/css/Components/_table-of-contents.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@use '_variables';
|
||||||
|
|
||||||
|
.table-of-contents {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 1em;
|
||||||
|
border: 1px solid var(--primary-border-color);
|
||||||
|
border-radius: var(--main-border-radius);
|
||||||
|
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
|
@ -12,3 +12,4 @@
|
||||||
@use 'Components/_lists';
|
@use 'Components/_lists';
|
||||||
@use 'Components/_code';
|
@use 'Components/_code';
|
||||||
@use 'Components/_posts';
|
@use 'Components/_posts';
|
||||||
|
@use 'Components/_table-of-contents';
|
||||||
|
|
167
src/posts/PluggableQueries.md
Normal file
167
src/posts/PluggableQueries.md
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
---
|
||||||
|
title: Pluggable Expressions In Entity Framework
|
||||||
|
description: The application structure I've been using as an alternative to the repository pattern
|
||||||
|
tags: [ Programming, C#, Entity Framework ]
|
||||||
|
---
|
||||||
|
|
||||||
|
[[toc]]
|
||||||
|
|
||||||
|
After maintaining a mid-sized application using a repository pattern, I've found this method to be more flexible and easier to test.
|
||||||
|
|
||||||
|
## Pluggable expressions
|
||||||
|
|
||||||
|
Expressions can be saved to variables (or returned by properties or functions) just like any object and just referenced in a query:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Expression<Func<Thing, bool>> registrationIsOpen =
|
||||||
|
t => t.RegistrationOpenDate < DateTime.Now
|
||||||
|
&& t.RegistrationCloseDate > DateTime.Now;
|
||||||
|
|
||||||
|
IList<Thing> openThings = db.Things
|
||||||
|
.Where(registrationIsOpen)
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
This also has the benefit of being very declarative; it's pretty easy to tell what the query is doing with `.Where(registrationIsOpen)` at a glance, even if you don't know much about `Thing` or the database structure. This becomes more of an advantage the more complicated the filter is.
|
||||||
|
|
||||||
|
These are also easy to test. Just create an in-memory list, run the expression against the list, and verify the results. Like with all EF queries though some expressions can't be translated, so you need to watch out for that.
|
||||||
|
|
||||||
|
You can have static classes filled with a bunch of pre-built expressions that you can pick from and apply as needed. If you have a one-off filter you need to use, then you can just directly use a lambda instead of a pre-built expression.
|
||||||
|
|
||||||
|
This can also be used on anything that `Where` can be called on. So if you already have a list, you can just reuse the same expression in a LINQ query on that list. Another useful case for this is map functions. Just like you can create expressions to pass to `.Where()`, you can also create expressions to pass to `.Select()`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ThingViewModel {
|
||||||
|
public int ThingId { get; set; }
|
||||||
|
public string ThingName { get; set; }
|
||||||
|
public DateTime RegistrationOpenDate { get; set; }
|
||||||
|
public DateTime RegistrationCloseDate { get; set; }
|
||||||
|
...
|
||||||
|
|
||||||
|
public static Expression<Func<Thing, ThingViewModel>> Map() {
|
||||||
|
return t => new ThingViewModel {
|
||||||
|
ThingId = t.ThingId,
|
||||||
|
ThingName = t.ThingName,
|
||||||
|
RegistrationOpenDate = t.RegistrationOpenDate,
|
||||||
|
RegistrationCloseDate = t.RegistrationCloseDate,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then to use it:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IList<ThingViewModel> things = db.Things
|
||||||
|
.Select(ThingViewModel.Map())
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested expressions
|
||||||
|
|
||||||
|
EF doesn't actually execute the LINQ query - it uses reflection to pick the query apart and translate it into a SQL query. Because of this, EF can only translate function calls and expressions it recognizes. This becomes a problem when you have nested expressions like this:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Expression<Func<Thing, bool>> isPastRegistrationOpenDate =
|
||||||
|
t => t.RegistrationOpenDate < DateTime.Now;
|
||||||
|
Expression<Func<Thing, bool>> isBeforeRegistrationCloseDate =
|
||||||
|
t => t.RegistrationCloseDate > DateTime.Now;
|
||||||
|
Expression<Func<Thing, bool>> registrationIsOpen =
|
||||||
|
t => isPastRegistrationOpenDate.Invoke(t)
|
||||||
|
&& isBeforeRegistrationCloseDate.Invoke(t);
|
||||||
|
|
||||||
|
IList<Thing> openThings = db.Things
|
||||||
|
.Where(registrationIsOpen)
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
This query will fail because Entity Framework can't doesn't have a translation for `isPastRegistrationOpenDate` and `isBeforeRegistrationCloseDate`.
|
||||||
|
|
||||||
|
In this case, the `registrationIsOpen` in `.Where(registrationIsOpen)` is replaced by the actual expression and EF will read the actual underlying expression. However, what EF sees when it tries to translate the query is references to two variables and an `Invoke` function that it doesn't understand.
|
||||||
|
|
||||||
|
The workaround for this is to use [LINQKit's `.AsExpandable()`](https://github.com/scottksmith95/LINQKit#plugging-expressions-into-entitysets--entitycollections-the-solution). By adding this to the query, it will alter the query and replace ("expand") the references to expressions with the expressions themselves. Then EF will see expressions that it understands.
|
||||||
|
|
||||||
|
So unlike the query above, this will work:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Expression<Func<Thing, bool>> isPastRegistrationOpenDate =
|
||||||
|
t => t.RegistrationOpenDate < DateTime.Now;
|
||||||
|
Expression<Func<Thing, bool>> isBeforeRegistrationCloseDate =
|
||||||
|
t => t.RegistrationCloseDate > DateTime.Now;
|
||||||
|
Expression<Func<Thing, bool>> registrationIsOpen =
|
||||||
|
t => isPastRegistrationOpenDate.Invoke(t)
|
||||||
|
&& isBeforeRegistrationCloseDate.Invoke(t);
|
||||||
|
|
||||||
|
IList<Thing> openThings = db.Things
|
||||||
|
.AsExpandable()
|
||||||
|
.Where(registrationIsOpen)
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it. Just add `.AsExpandable()` to your query and it will work.
|
||||||
|
|
||||||
|
Another example using the above view model example:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ThingExpressions {
|
||||||
|
public static Expression<Func<Thing, bool>> IsPastRegistrationOpenDate() {
|
||||||
|
return t => t.RegistrationOpenDate < DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression<Func<Thing, bool>> IsBeforeRegistrationCloseDate() {
|
||||||
|
return t => t.RegistrationCloseDate > DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression<Func<Thing, bool>> RegistrationIsOpen() {
|
||||||
|
return t => IsPastRegistrationOpenDate.Invoke(t)
|
||||||
|
&& IsBeforeRegistrationCloseDate.Invoke(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThingViewModel {
|
||||||
|
public int ThingId { get; set; }
|
||||||
|
public string ThingName { get; set; }
|
||||||
|
public bool IsRegistrationOpen { get; set; }
|
||||||
|
|
||||||
|
public static Expression<Func<Thing, ThingViewModel>> Map() {
|
||||||
|
return t => new ThingViewModel {
|
||||||
|
ThingId = t.ThingId,
|
||||||
|
ThingName = t.ThingName,
|
||||||
|
// This will throw an exception if AsExpandable isn't used
|
||||||
|
IsRegistrationOpen = ThingExpressions.RegistrationIsOpen().Invoke(t)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then to use it:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IList<ThingViewModel> things = db.Things
|
||||||
|
.AsExpandable()
|
||||||
|
.Select(ThingViewModel.Map())
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compared to repositories
|
||||||
|
|
||||||
|
The usual way to centralize filters for database queries with Entity Framework is to use repositories with methods like this:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public IList<Thing> GetThings(bool whereRegistrationOpen = false) {
|
||||||
|
var query = this.db.Things
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (whereRegistrationOpen)
|
||||||
|
query = query
|
||||||
|
.Where(t => t.RegistrationOpenDate < DateTime.Now)
|
||||||
|
.Where(t => t.RegistrationCloseDate > DateTime.Now);
|
||||||
|
|
||||||
|
return query.ToList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However, for large, complex objects this can quickly bloat the function with dozens of options. This makes the method very large (potentially hundreds of lines) and more awkward to ensure that tests each only test a specific piece of the method.
|
||||||
|
|
||||||
|
Also, the filters themselves are sealed in the repository method. You can't use them in a different way than the repository is built for (like applying the filter to an in-memory list or using them to set a property in a `.Select()`).
|
Loading…
Reference in a new issue