From d274352b5f048e91321038bd896069431e55e160 Mon Sep 17 00:00:00 2001 From: benjaminramey Date: Mon, 21 Sep 2020 21:56:11 -0500 Subject: [PATCH] WIP, psts in place, parsing mostly correclty --- gatsby-config.js | 12 +- gatsby-node.js | 43 + gatsby-ssr.js | 9 + package-lock.json | 847 ++++++++++++++++++ package.json | 3 + src/components/footer.js | 22 + src/components/header.js | 7 +- src/components/layout.js | 14 +- src/components/nav.js | 17 + src/pages/index.js | 6 +- ...ng-newline-characters-in-vb-net-strings.md | 20 + ...-color-to-a-system-drawing-color-object.md | 18 + .../2010/06/29/vertical-text-with-css.md | 13 + ...al-studio-post-build-event-to-copy-dlls.md | 26 + ...arepoint-moss-2007-service-pack-version.md | 20 + ...upgrade-servicehostingenvironment-error.md | 29 + ...ng-and-connected-to-the-sharepoint-farm.md | 15 + ...request-object-always-datetime-minvalue.md | 13 + ...min-web-applications-page-ribbon-button.md | 17 + .../12/01/blank-sharepoint-site-problem.md | 13 + .../duplicating-spwebconfigmodifications.md | 17 + ...arepoint-2010-documents-in-a-new-window.md | 52 ++ .../08/24/file-name-sanitizer-one-liner.md | 19 + .../2011/11/14/ansiutf-8-encoding-problem.md | 34 + .../11/21/jquery-tools-overlay-v1-2-5-bug.md | 21 + ...formatting-multiple-xml-files-at-a-time.md | 34 + ...create-a-brightcove-player-logo-overlay.md | 56 ++ .../02/03/install-sharepoint-on-windows-7.md | 21 + .../2012/02/27/ektron-license-key-update.md | 19 + .../03/05/grouping-into-rows-with-xslt.md | 168 ++++ ...-way-to-make-huge-maps-with-google-maps.md | 10 + .../change-the-color-of-an-icon-with-gimp.md | 69 ++ ...ected-items-on-sitecore-multilist-field.md | 22 + src/posts/2012/07/19/grace-and-me.md | 43 + .../25/nested-user-controls-not-rendering.md | 25 + .../27/group-list-into-sub-lists-by-index.md | 22 + ...ome-as-its-parent-got-rendering-instead.md | 43 + ...ted-error-when-deleting-sharepoint-site.md | 13 + ...10-page-layouts-with-visual-studio-2010.md | 9 + ...ent-page-that-contains-content-controls.md | 9 + .../27/cannot-remove-file-error-code-158.md | 78 ++ ...-prompts-for-sharepoint-site-collection.md | 9 + .../check-for-a-publishing-spweb-or-spsite.md | 9 + ...hoice-field-value-options-in-sitefinity.md | 104 +++ ...reating-and-activating-a-dynamic-module.md | 239 +++++ .../12/custom-error-pages-with-sitefinity.md | 32 + ...nomy-fields-for-search-in-sitefinity-51.md | 113 +++ ...efinity-5-1-pagesselector-in-javascript.md | 114 +++ .../01/15/linq-to-ektron-search-library.md | 36 + ...ace-expression-to-a-concrete-expression.md | 74 ++ src/posts/2013/03/15/ektron-paths.md | 45 + ...er-wcf-error-in-ektron-8-5-3-tier-setup.md | 43 + ...ncodes-certain-content-title-characters.md | 21 + ...ccountlogin-with-windows-authentication.md | 9 + ...cks-and-sharepoint-package-interference.md | 13 + .../sharepoint-2010-401-unauthorized-error.md | 71 ++ ...-linq-spmetal-in-anonymous-user-context.md | 13 + ...int-unknown-error-after-database-attach.md | 15 + ...ating-or-editing-a-sharepoint-list-view.md | 74 ++ ...-up-sharepoint-2010-search-pdf-indexing.md | 9 + .../15/querying-a-lookup-field-with-caml.md | 9 + ...010-page-layout-button-javascript-error.md | 18 + .../14/how-not-to-validate-email-addresses.md | 9 + ...epoint-2010-language-packs-on-windows-7.md | 9 + src/posts/2014/02/23/better-gruntfiles.md | 9 + ...vicebus-with-entityframework-persisters.md | 82 ++ .../03/21/the-dumbest-code-i-ever-wrote.md | 14 + ...file-linkguid-aspx-does-not-exist-error.md | 32 + .../17/episerver-7-cannot-decrypt-password.md | 47 + ...-in-a-widget-designer-in-sitefinity-6-2.md | 133 +++ ...esigner-field-auto-generation-is-flawed.md | 32 + ...ie-date-parsing-doesnt-work-like-chrome.md | 30 + .../23/apply-git-patches-to-svn-repository.md | 10 + ...ntication-and-group-based-authorization.md | 188 ++++ .../permutations-with-iterators-in-csharp.md | 25 + .../working-with-x509-certificates-in-net.md | 16 + ...-file-when-running-as-a-windows-service.md | 43 + ...nt-subscribe-to-ievent-with-nservicebus.md | 86 ++ ...ackager-0.5.5-with-gitversiontask-3.4.1.md | 74 ++ ...h-integration-testing-with-bearer-token.md | 92 ++ ...dfactoryfacility-and-property-injection.md | 133 +++ ...That-Expect-Me-To-Be-a-Great-Programmer.md | 40 + src/posts/2020/05/12/psalm-130.md | 147 +++ .../27/iterating-all-catalog-metaobjects.md | 462 ++++++++++ src/posts/organize.ps1 | 16 + src/templates/page.js | 13 + src/templates/post.js | 71 ++ 87 files changed, 4820 insertions(+), 11 deletions(-) create mode 100644 gatsby-node.js create mode 100644 gatsby-ssr.js create mode 100644 src/components/footer.js create mode 100644 src/components/nav.js create mode 100644 src/posts/2009/09/01/replacing-newline-characters-in-vb-net-strings.md create mode 100644 src/posts/2009/09/24/converting-a-hexadecimal-color-to-a-system-drawing-color-object.md create mode 100644 src/posts/2010/06/29/vertical-text-with-css.md create mode 100644 src/posts/2010/06/29/visual-studio-post-build-event-to-copy-dlls.md create mode 100644 src/posts/2010/10/06/determine-sharepoint-moss-2007-service-pack-version.md create mode 100644 src/posts/2010/10/06/sharepoint-2007-to-2010-in-place-upgrade-servicehostingenvironment-error.md create mode 100644 src/posts/2010/10/08/error-occurred-in-deployment-step-recycle-iis-application-pool-the-local-sharepoint-server-is-not-available-check-that-the-server-is-running-and-connected-to-the-sharepoint-farm.md create mode 100644 src/posts/2010/10/12/cookie-expires-from-asp-net-request-object-always-datetime-minvalue.md create mode 100644 src/posts/2010/11/02/add-sharepoint-2010-central-admin-web-applications-page-ribbon-button.md create mode 100644 src/posts/2010/12/01/blank-sharepoint-site-problem.md create mode 100644 src/posts/2010/12/10/duplicating-spwebconfigmodifications.md create mode 100644 src/posts/2011/04/25/opening-all-sharepoint-2010-documents-in-a-new-window.md create mode 100644 src/posts/2011/08/24/file-name-sanitizer-one-liner.md create mode 100644 src/posts/2011/11/14/ansiutf-8-encoding-problem.md create mode 100644 src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md create mode 100644 src/posts/2011/12/14/formatting-multiple-xml-files-at-a-time.md create mode 100644 src/posts/2012/02/03/how-to-create-a-brightcove-player-logo-overlay.md create mode 100644 src/posts/2012/02/03/install-sharepoint-on-windows-7.md create mode 100644 src/posts/2012/02/27/ektron-license-key-update.md create mode 100644 src/posts/2012/03/05/grouping-into-rows-with-xslt.md create mode 100644 src/posts/2012/03/07/easy-way-to-make-huge-maps-with-google-maps.md create mode 100644 src/posts/2012/03/15/change-the-color-of-an-icon-with-gimp.md create mode 100644 src/posts/2012/06/12/limit-selected-items-on-sitecore-multilist-field.md create mode 100644 src/posts/2012/07/19/grace-and-me.md create mode 100644 src/posts/2012/07/25/nested-user-controls-not-rendering.md create mode 100644 src/posts/2012/07/27/group-list-into-sub-lists-by-index.md create mode 100644 src/posts/2012/08/09/rendering-must-have-placeholder-chrome-as-its-parent-got-rendering-instead.md create mode 100644 src/posts/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site.md create mode 100644 src/posts/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010.md create mode 100644 src/posts/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls.md create mode 100644 src/posts/2012/09/27/cannot-remove-file-error-code-158.md create mode 100644 src/posts/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection.md create mode 100644 src/posts/2012/09/28/check-for-a-publishing-spweb-or-spsite.md create mode 100644 src/posts/2012/10/05/retrieve-dynamic-module-choice-field-value-options-in-sitefinity.md create mode 100644 src/posts/2012/10/10/sitefinity-configuration-file-changes-when-creating-and-activating-a-dynamic-module.md create mode 100644 src/posts/2012/10/12/custom-error-pages-with-sitefinity.md create mode 100644 src/posts/2012/10/19/indexing-taxonomy-fields-for-search-in-sitefinity-51.md create mode 100644 src/posts/2012/10/25/setting-selected-items-on-sitefinity-5-1-pagesselector-in-javascript.md create mode 100644 src/posts/2013/01/15/linq-to-ektron-search-library.md create mode 100644 src/posts/2013/01/23/converting-an-interface-expression-to-a-concrete-expression.md create mode 100644 src/posts/2013/03/15/ektron-paths.md create mode 100644 src/posts/2013/03/15/taxonomymanager-wcf-error-in-ektron-8-5-3-tier-setup.md create mode 100644 src/posts/2013/03/20/ektron-html-encodes-certain-content-title-characters.md create mode 100644 src/posts/2013/04/23/asp-net-mvc-3-0-redirect-to-accountlogin-with-windows-authentication.md create mode 100644 src/posts/2013/05/08/sitecore-rocks-and-sharepoint-package-interference.md create mode 100644 src/posts/2013/05/23/sharepoint-2010-401-unauthorized-error.md create mode 100644 src/posts/2013/05/23/sharepoint-linq-spmetal-in-anonymous-user-context.md create mode 100644 src/posts/2013/06/06/sharepoint-unknown-error-after-database-attach.md create mode 100644 src/posts/2013/06/13/error-on-_vti_binowssvr-dllcs65001-when-creating-or-editing-a-sharepoint-list-view.md create mode 100644 src/posts/2013/06/26/properly-setting-up-sharepoint-2010-search-pdf-indexing.md create mode 100644 src/posts/2013/07/15/querying-a-lookup-field-with-caml.md create mode 100644 src/posts/2013/09/25/sharepoint-2010-page-layout-button-javascript-error.md create mode 100644 src/posts/2013/10/14/how-not-to-validate-email-addresses.md create mode 100644 src/posts/2013/11/05/installing-sharepoint-2010-language-packs-on-windows-7.md create mode 100644 src/posts/2014/02/23/better-gruntfiles.md create mode 100644 src/posts/2014/03/07/nservicebus-with-entityframework-persisters.md create mode 100644 src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md create mode 100644 src/posts/2014/03/27/episerver-7-the-file-linkguid-aspx-does-not-exist-error.md create mode 100644 src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md create mode 100644 src/posts/2014/05/21/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2.md create mode 100644 src/posts/2014/05/23/sitefinity-thunder-boolean-widget-designer-field-auto-generation-is-flawed.md create mode 100644 src/posts/2014/06/05/ie-date-parsing-doesnt-work-like-chrome.md create mode 100644 src/posts/2014/09/23/apply-git-patches-to-svn-repository.md create mode 100644 src/posts/2014/10/20/active-directory-authentication-in-asp-net-mvc-5-with-forms-authentication-and-group-based-authorization.md create mode 100644 src/posts/2015/09/03/permutations-with-iterators-in-csharp.md create mode 100644 src/posts/2015/12/02/working-with-x509-certificates-in-net.md create mode 100644 src/posts/2016/01/04/adding-a-rabbitmq-configuration-file-when-running-as-a-windows-service.md create mode 100644 src/posts/2016/02/25/you-cant-subscribe-to-ievent-with-nservicebus.md create mode 100644 src/posts/2016/03/03/using-nugetpackager-0.5.5-with-gitversiontask-3.4.1.md create mode 100644 src/posts/2016/04/11/owin-oauth-integration-testing-with-bearer-token.md create mode 100644 src/posts/2016/08/24/castle-windsor-typedfactoryfacility-and-property-injection.md create mode 100644 src/posts/2017/01/20/I-Like-Languages-That-Expect-Me-To-Be-a-Great-Programmer.md create mode 100644 src/posts/2020/05/12/psalm-130.md create mode 100644 src/posts/2020/05/27/iterating-all-catalog-metaobjects.md create mode 100644 src/posts/organize.ps1 create mode 100644 src/templates/page.js create mode 100644 src/templates/post.js diff --git a/gatsby-config.js b/gatsby-config.js index ae47413..20c4072 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -6,5 +6,15 @@ module.exports = { /* Your site config here */ - plugins: [`gatsby-plugin-sass`], + plugins: [ + `gatsby-plugin-sass`, + { + resolve: `gatsby-source-filesystem`, + options: { + name: `posts`, + path: `${__dirname}/src/posts`, + }, + }, + `gatsby-transformer-remark` + ], } diff --git a/gatsby-node.js b/gatsby-node.js new file mode 100644 index 0000000..0e11758 --- /dev/null +++ b/gatsby-node.js @@ -0,0 +1,43 @@ +const path = require(`path`); +const { createFilePath } = require(`gatsby-source-filesystem`); + +exports.onCreateNode = ({ node, getNode, actions }) => { + const { createNodeField } = actions; + + if (node.internal.type === `MarkdownRemark`) { + var slug = createFilePath({ node, getNode, basePath: `pages` }); + createNodeField({ + node, + name: `slug`, + value: slug, + }); + } +}; + +exports.createPages = async ({ graphql, actions }) => { + const { createPage } = actions; + + const result = await graphql(` + query { + allMarkdownRemark { + edges { + node { + fields { + slug + } + } + } + } + } + `); + + result.data.allMarkdownRemark.edges.forEach(({ node }) => { + createPage({ + path: node.fields.slug, + component: path.resolve(`./src/templates/post.js`), + context: { + slug: node.fields.slug + } + }); + }); +}; \ No newline at end of file diff --git a/gatsby-ssr.js b/gatsby-ssr.js new file mode 100644 index 0000000..63a7938 --- /dev/null +++ b/gatsby-ssr.js @@ -0,0 +1,9 @@ +import React from "react"; + +export const onRenderBody = ({ setPostBodyComponents }) => { + setPostBodyComponents([ + , + , + + ]) + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bf8d268..d26c3de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1995,6 +1995,24 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, + "@types/vfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-3.0.2.tgz", + "integrity": "sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==", + "requires": { + "@types/node": "*", + "@types/unist": "*", + "@types/vfile-message": "*" + } + }, + "@types/vfile-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/vfile-message/-/vfile-message-2.0.0.tgz", + "integrity": "sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==", + "requires": { + "vfile-message": "*" + } + }, "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -2498,6 +2516,11 @@ } } }, + "array-iterate": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-1.1.4.tgz", + "integrity": "sha512-sNRaPGh9nnmdC8Zf+pT3UqP8rnWj5Hf9wiFGsX3wUQ2yVSIhO2ShFwCoceIPpB41QF6i2OEmrHmCo36xronCVA==" + }, "array-map": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", @@ -4769,6 +4792,11 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + }, "css-selector-tokenizer": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", @@ -5340,6 +5368,11 @@ "path-type": "^4.0.0" } }, + "disqus-react": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/disqus-react/-/disqus-react-1.0.10.tgz", + "integrity": "sha512-TFd+pSl9m9QpqGcNAmewH/V795I9ObJMcEbLCIyPBy1VvUUl8DUoP+OiWhx24UjRygfV8y6saZVTBJRNYGJvkQ==" + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -6587,6 +6620,11 @@ } } }, + "file-type": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", + "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7742,6 +7780,56 @@ } } }, + "gatsby-source-filesystem": { + "version": "2.3.30", + "resolved": "https://registry.npmjs.org/gatsby-source-filesystem/-/gatsby-source-filesystem-2.3.30.tgz", + "integrity": "sha512-jZ6fWTT0a/aUMmhEyGcxEc8IdTJvxA/806qkan/5Mhctt4tjqmjSq8TWC0yde+0uHBWBf9krbzKtuT/+hT9KuA==", + "requires": { + "@babel/runtime": "^7.11.2", + "better-queue": "^3.8.10", + "bluebird": "^3.7.2", + "chokidar": "^3.4.2", + "file-type": "^12.4.2", + "fs-extra": "^8.1.0", + "gatsby-core-utils": "^1.3.20", + "got": "^9.6.0", + "md5-file": "^3.2.3", + "mime": "^2.4.6", + "pretty-bytes": "^5.3.0", + "progress": "^2.0.3", + "read-chunk": "^3.2.0", + "valid-url": "^1.0.9", + "xstate": "^4.11.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + } + } + }, "gatsby-telemetry": { "version": "1.3.35", "resolved": "https://registry.npmjs.org/gatsby-telemetry/-/gatsby-telemetry-1.3.35.tgz", @@ -7771,6 +7859,290 @@ } } }, + "gatsby-transformer-remark": { + "version": "2.8.35", + "resolved": "https://registry.npmjs.org/gatsby-transformer-remark/-/gatsby-transformer-remark-2.8.35.tgz", + "integrity": "sha512-dIzl5OKgH3eeH8mJZ45GxgUVbClGp9VulGVVhJAnG7H5v3AMMey8JGVoUyECbAd42HH+2YoebYrvsEKExYeHFA==", + "requires": { + "@babel/runtime": "^7.11.2", + "bluebird": "^3.7.2", + "gatsby-core-utils": "^1.3.20", + "gray-matter": "^4.0.2", + "hast-util-raw": "^4.0.0", + "hast-util-to-html": "^4.0.1", + "lodash": "^4.17.20", + "mdast-util-to-hast": "^3.0.4", + "mdast-util-to-string": "^1.1.0", + "mdast-util-toc": "^5.0", + "remark": "^10.0.1", + "remark-parse": "^6.0.3", + "remark-retext": "^3.1.3", + "remark-stringify": "6.0.4", + "retext-english": "^3.0.4", + "sanitize-html": "^1.27.0", + "underscore.string": "^3.3.5", + "unified": "^6.2.0", + "unist-util-remove-position": "^1.1.4", + "unist-util-select": "^1.5.0", + "unist-util-visit": "^1.4.1" + }, + "dependencies": { + "hast-to-hyperscript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-5.0.0.tgz", + "integrity": "sha512-DLl3eYTz8uwwzEubDUdCChsR5t5b2ne+yvHrA2h58Suq/JnN3+Gsb9Tc4iZoCCsykmFUc6UUpwxTmQXs0akSeg==", + "requires": { + "comma-separated-tokens": "^1.0.0", + "property-information": "^4.0.0", + "space-separated-tokens": "^1.0.0", + "style-to-object": "^0.2.1", + "unist-util-is": "^2.0.0", + "web-namespaces": "^1.1.2" + } + }, + "hast-util-from-parse5": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-4.0.2.tgz", + "integrity": "sha512-I6dtjsGtDqz4fmGSiFClFyiXdKhj5bPceS6intta7k/VDuiKz9P61C6hO6WMiNNmEm1b/EtBH8f+juvz4o0uwQ==", + "requires": { + "ccount": "^1.0.3", + "hastscript": "^4.0.0", + "property-information": "^4.0.0", + "web-namespaces": "^1.1.2", + "xtend": "^4.0.1" + } + }, + "hast-util-raw": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-4.0.0.tgz", + "integrity": "sha512-5xYHyEJMCf8lX/NT4iA5z6N43yoFsrJqXJ5GWwAbLn815URbIz+UNNFEgid33F9paZuDlqVKvB+K3Aqu5+DdSw==", + "requires": { + "hast-util-from-parse5": "^4.0.2", + "hast-util-to-parse5": "^4.0.1", + "html-void-elements": "^1.0.1", + "parse5": "^5.0.0", + "unist-util-position": "^3.0.0", + "web-namespaces": "^1.0.0", + "xtend": "^4.0.1", + "zwitch": "^1.0.0" + } + }, + "hast-util-to-parse5": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-4.0.1.tgz", + "integrity": "sha512-U/61W+fsNfBpCyJBB5Pt3l5ypIfgXqEyW9pyrtxF7XrqDJHzcFrYpnC94d0JDYjvobLpYCzcU9srhMRPEO1YXw==", + "requires": { + "hast-to-hyperscript": "^5.0.0", + "property-information": "^4.0.0", + "web-namespaces": "^1.0.0", + "xtend": "^4.0.1", + "zwitch": "^1.0.0" + } + }, + "hastscript": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-4.1.0.tgz", + "integrity": "sha512-bOTn9hEfzewvHyXdbYGKqOr/LOz+2zYhKbC17U2YAjd16mnjqB1BQ0nooM/RdMy/htVyli0NAznXiBtwDi1cmQ==", + "requires": { + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.2.0", + "property-information": "^4.0.0", + "space-separated-tokens": "^1.0.0" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" + }, + "mdast-util-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", + "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "mdast-util-definitions": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-1.2.5.tgz", + "integrity": "sha512-CJXEdoLfiISCDc2JB6QLb79pYfI6+GcIH+W2ox9nMc7od0Pz+bovcHsiq29xAQY6ayqe/9CsK2VzkSJdg1pFYA==", + "requires": { + "unist-util-visit": "^1.0.0" + } + }, + "mdast-util-to-hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz", + "integrity": "sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA==", + "requires": { + "collapse-white-space": "^1.0.0", + "detab": "^2.0.0", + "mdast-util-definitions": "^1.2.0", + "mdurl": "^1.0.1", + "trim": "0.0.1", + "trim-lines": "^1.0.0", + "unist-builder": "^1.0.1", + "unist-util-generated": "^1.1.0", + "unist-util-position": "^3.0.0", + "unist-util-visit": "^1.1.0", + "xtend": "^4.0.1" + } + }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "property-information": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-4.2.0.tgz", + "integrity": "sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==", + "requires": { + "xtend": "^4.0.1" + } + }, + "remark-stringify": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", + "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", + "requires": { + "ccount": "^1.0.0", + "is-alphanumeric": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "longest-streak": "^2.0.1", + "markdown-escapes": "^1.0.0", + "markdown-table": "^1.1.0", + "mdast-util-compact": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "stringify-entities": "^1.0.1", + "unherit": "^1.0.4", + "xtend": "^4.0.1" + } + }, + "stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "requires": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "style-to-object": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.2.3.tgz", + "integrity": "sha512-1d/k4EY2N7jVLOqf2j04dTc37TPOv/hHxZmvpg8Pdh8UYydxeu/C1W1U4vD8alzf5V2Gt7rLsmkr4dxAlDm9ng==", + "requires": { + "inline-style-parser": "0.1.1" + } + }, + "unified": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", + "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "requires": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^2.0.0", + "x-is-string": "^0.1.0" + } + }, + "unist-builder": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-1.0.4.tgz", + "integrity": "sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg==", + "requires": { + "object-assign": "^4.1.0" + } + }, + "unist-util-is": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz", + "integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA==" + }, + "unist-util-remove-position": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", + "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "requires": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "requires": { + "unist-util-is": "^3.0.0" + }, + "dependencies": { + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + } + } + }, + "vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "requires": { + "is-buffer": "^1.1.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + } + } + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -7869,6 +8241,21 @@ "parse-url": "^5.0.0" } }, + "github-slugger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz", + "integrity": "sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q==", + "requires": { + "emoji-regex": ">=6.0.0 <=6.1.1" + }, + "dependencies": { + "emoji-regex": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz", + "integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=" + } + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -8157,6 +8544,17 @@ "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==" }, + "gray-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz", + "integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==", + "requires": { + "js-yaml": "^3.11.0", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + } + }, "gud": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", @@ -8359,6 +8757,11 @@ "web-namespaces": "^1.0.0" } }, + "hast-util-is-element": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz", + "integrity": "sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==" + }, "hast-util-parse-selector": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz", @@ -8381,6 +8784,49 @@ "zwitch": "^1.0.0" } }, + "hast-util-to-html": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-4.0.1.tgz", + "integrity": "sha512-2emzwyf0xEsc4TBIPmDJmBttIw8R4SXAJiJZoiRR/s47ODYWgOqNoDbf2SJAbMbfNdFWMiCSOrI3OVnX6Qq2Mg==", + "requires": { + "ccount": "^1.0.0", + "comma-separated-tokens": "^1.0.1", + "hast-util-is-element": "^1.0.0", + "hast-util-whitespace": "^1.0.0", + "html-void-elements": "^1.0.0", + "property-information": "^4.0.0", + "space-separated-tokens": "^1.0.0", + "stringify-entities": "^1.0.1", + "unist-util-is": "^2.0.0", + "xtend": "^4.0.1" + }, + "dependencies": { + "property-information": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-4.2.0.tgz", + "integrity": "sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==", + "requires": { + "xtend": "^4.0.1" + } + }, + "stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "requires": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "unist-util-is": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.3.tgz", + "integrity": "sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA==" + } + } + }, "hast-util-to-parse5": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz", @@ -8393,6 +8839,11 @@ "zwitch": "^1.0.0" } }, + "hast-util-whitespace": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz", + "integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==" + }, "hastscript": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", @@ -10142,6 +10593,43 @@ "unist-util-visit": "^2.0.0" } }, + "mdast-util-to-nlcst": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/mdast-util-to-nlcst/-/mdast-util-to-nlcst-3.2.3.tgz", + "integrity": "sha512-hPIsgEg7zCvdU6/qvjcR6lCmJeRuIEpZGY5xBV+pqzuMOvQajyyF8b6f24f8k3Rw8u40GwkI3aAxUXr3bB2xag==", + "requires": { + "nlcst-to-string": "^2.0.0", + "repeat-string": "^1.5.2", + "unist-util-position": "^3.0.0", + "vfile-location": "^2.0.0" + }, + "dependencies": { + "vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==" + } + } + }, + "mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==" + }, + "mdast-util-toc": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-toc/-/mdast-util-toc-5.0.3.tgz", + "integrity": "sha512-A3xzcgC1XFHK0+abFmbINOxjwo7Bi0Nsfp3yTgTy5JHo2q2V6YZ5BVJreDWoK3szcLlSMvHqe8WPbjY50wAkow==", + "requires": { + "@types/mdast": "^3.0.3", + "@types/unist": "^2.0.3", + "extend": "^3.0.2", + "github-slugger": "^1.2.1", + "mdast-util-to-string": "^1.0.5", + "unist-util-is": "^4.0.0", + "unist-util-visit": "^2.0.0" + } + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -10601,6 +11089,11 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "nlcst-to-string": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz", + "integrity": "sha512-3x3jwTd6UPG7vi5k4GEzvxJ5rDA7hVUIRNHPblKuMVP9Z3xmlsd9cgLcpAMkc5uPOBna82EeshROFhsPkbnTZg==" + }, "no-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", @@ -11435,6 +11928,17 @@ "safe-buffer": "^5.1.1" } }, + "parse-english": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/parse-english/-/parse-english-4.1.3.tgz", + "integrity": "sha512-IQl1v/ik9gw437T8083coohMihae0rozpc7JYC/9h6hi9xKBSxFwh5HWRpzVC2ZhEs2nUlze2aAktpNBJXdJKA==", + "requires": { + "nlcst-to-string": "^2.0.0", + "parse-latin": "^4.0.0", + "unist-util-modify-children": "^1.0.0", + "unist-util-visit-children": "^1.0.0" + } + }, "parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -11459,6 +11963,16 @@ "lines-and-columns": "^1.1.6" } }, + "parse-latin": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-4.2.1.tgz", + "integrity": "sha512-7T9g6mIsFFpLlo0Zzb2jLWdCt+H9Qtf/hRmMYFi/Mq6Ovi+YKo+AyDFX3OhFfu0vXX5Nid9FKJGKSSzNcTkWiA==", + "requires": { + "nlcst-to-string": "^2.0.0", + "unist-util-modify-children": "^1.0.0", + "unist-util-visit-children": "^1.0.0" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -11473,6 +11987,11 @@ "protocols": "^1.4.0" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "parse-url": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", @@ -12357,6 +12876,11 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==" }, + "pretty-bytes": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", + "integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==" + }, "pretty-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", @@ -13023,6 +13547,22 @@ "mute-stream": "~0.0.4" } }, + "read-chunk": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", + "integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==", + "requires": { + "pify": "^4.0.1", + "with-open-file": "^0.1.6" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + } + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -13293,6 +13833,146 @@ } } }, + "remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-10.0.1.tgz", + "integrity": "sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==", + "requires": { + "remark-parse": "^6.0.0", + "remark-stringify": "^6.0.0", + "unified": "^7.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" + }, + "mdast-util-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz", + "integrity": "sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==", + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "remark-stringify": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", + "integrity": "sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==", + "requires": { + "ccount": "^1.0.0", + "is-alphanumeric": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "longest-streak": "^2.0.1", + "markdown-escapes": "^1.0.0", + "markdown-table": "^1.1.0", + "mdast-util-compact": "^1.0.0", + "parse-entities": "^1.0.2", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "stringify-entities": "^1.0.1", + "unherit": "^1.0.4", + "xtend": "^4.0.1" + } + }, + "stringify-entities": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", + "integrity": "sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==", + "requires": { + "character-entities-html4": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "unified": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-7.1.0.tgz", + "integrity": "sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==", + "requires": { + "@types/unist": "^2.0.0", + "@types/vfile": "^3.0.0", + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^3.0.0", + "x-is-string": "^0.1.0" + } + }, + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "requires": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "requires": { + "unist-util-is": "^3.0.0" + } + }, + "vfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.1.tgz", + "integrity": "sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==", + "requires": { + "is-buffer": "^2.0.0", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + } + } + }, "remark-footnotes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-1.0.0.tgz", @@ -13436,6 +14116,14 @@ } } }, + "remark-retext": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/remark-retext/-/remark-retext-3.1.3.tgz", + "integrity": "sha512-UujXAm28u4lnUvtOZQFYfRIhxX+auKI9PuA2QpQVTT7gYk1OgX6o0OUrSo1KOa6GNrFX+OODOtS5PWIHPxM7qw==", + "requires": { + "mdast-util-to-nlcst": "^3.2.0" + } + }, "remark-squeeze-paragraphs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz", @@ -13628,6 +14316,15 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "retext-english": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/retext-english/-/retext-english-3.0.4.tgz", + "integrity": "sha512-yr1PgaBDde+25aJXrnt3p1jvT8FVLVat2Bx8XeAWX13KXo8OT+3nWGU3HWxM4YFJvmfqvJYJZG2d7xxaO774gw==", + "requires": { + "parse-english": "^4.0.0", + "unherit": "^1.0.4" + } + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -13709,6 +14406,63 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-html": { + "version": "1.27.4", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.4.tgz", + "integrity": "sha512-VvY1hxVvMXzSos/LzqeBl9/KYu3mkEOtl5NMwz6jER318dSHDCig0AOjZOtnoCwAC3HMs9LhfWkPCmQGttb4ng==", + "requires": { + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15", + "parse-srcset": "^1.0.2", + "postcss": "^7.0.27" + }, + "dependencies": { + "dom-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz", + "integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", + "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==" + }, + "domhandler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", + "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.3.0.tgz", + "integrity": "sha512-xWC75PM3QF6MjE5e58OzwTX0B/rPQnlqH0YyXB/c056RtVJA+eu60da2I/bdnEHzEYC00g8QaZUlAbqOZVbOsw==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0" + } + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + } + } + }, "sass-graph": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", @@ -13787,6 +14541,25 @@ } } }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14792,6 +15565,11 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -15357,6 +16135,15 @@ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -15462,6 +16249,14 @@ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz", "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==" }, + "unist-util-modify-children": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-1.1.6.tgz", + "integrity": "sha512-TOA6W9QLil+BrHqIZNR4o6IA5QwGOveMbnQxnWYq+7EFORx9vz/CHrtzF36zWrW61E2UKw7sM1KPtIgeceVwXw==", + "requires": { + "array-iterate": "^1.0.0" + } + }, "unist-util-position": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", @@ -15483,6 +16278,26 @@ "unist-util-visit": "^2.0.0" } }, + "unist-util-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-1.5.0.tgz", + "integrity": "sha1-qTwr6MD2U4J4A7gTMa3sKqJM2TM=", + "requires": { + "css-selector-parser": "^1.1.0", + "debug": "^2.2.0", + "nth-check": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -15501,6 +16316,11 @@ "unist-util-visit-parents": "^3.0.0" } }, + "unist-util-visit-children": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz", + "integrity": "sha512-sA/nXwYRCQVRwZU2/tQWUqJ9JSFM1X3x7JIOsIgSzrFHcfVt6NkzDtKzyxg2cZWkCwGF9CO8x4QNZRJRMK8FeQ==" + }, "unist-util-visit-parents": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz", @@ -15789,6 +16609,11 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz", "integrity": "sha512-ejdrifsIydN1XDH7EuR2hn8ZrkRKUYF7tUcBjBy/lhrCvs2K+zRlbW9UHc0IQ9RsYFZJFqJrieoIHfkCa0DBRA==" }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -16351,6 +17176,23 @@ } } }, + "with-open-file": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz", + "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==", + "requires": { + "p-finally": "^1.0.0", + "p-try": "^2.1.0", + "pify": "^4.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + } + } + }, "wonka": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.14.tgz", @@ -16431,6 +17273,11 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", diff --git a/package.json b/package.json index 72ac975..abb2b72 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" }, "dependencies": { + "disqus-react": "^1.0.10", "gatsby": "^2.24.61", "gatsby-plugin-sass": "^2.3.13", + "gatsby-source-filesystem": "^2.3.30", + "gatsby-transformer-remark": "^2.8.35", "node-sass": "^4.14.1", "react": "^16.12.0", "react-dom": "^16.12.0" diff --git a/src/components/footer.js b/src/components/footer.js new file mode 100644 index 0000000..3714022 --- /dev/null +++ b/src/components/footer.js @@ -0,0 +1,22 @@ +import React from "react"; + +export default function Footer() { + const date = new Date(); + const timestamp = date.toISOString(); + const year = date.getYear() + 1900; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/header.js b/src/components/header.js index d77b413..fc282fd 100644 --- a/src/components/header.js +++ b/src/components/header.js @@ -5,17 +5,16 @@ export default function Header() {
- + Gravatar - site.title + Ben Ramey's Blog
- site.tagline + Scripture, programming problems, solutions and stories.
); } - diff --git a/src/components/layout.js b/src/components/layout.js index 43232fb..d1db325 100644 --- a/src/components/layout.js +++ b/src/components/layout.js @@ -1,11 +1,19 @@ import React from "react"; import Header from './header'; +import Footer from './footer'; +import Nav from './nav'; export default function Layout({ children }) { return ( - +
- {children} - +
); } \ No newline at end of file diff --git a/src/components/nav.js b/src/components/nav.js new file mode 100644 index 0000000..c3b222d --- /dev/null +++ b/src/components/nav.js @@ -0,0 +1,17 @@ +import React from "react"; + +export default function Nav() { + return ( + + ); +} \ No newline at end of file diff --git a/src/pages/index.js b/src/pages/index.js index 0cd41b6..df3fe1b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,10 +1,10 @@ import React from "react"; -import Layout from "../components/layout" +import Page from "../templates/page"; export default function Home() { return ( - +
hello world!
-
+ ); } diff --git a/src/posts/2009/09/01/replacing-newline-characters-in-vb-net-strings.md b/src/posts/2009/09/01/replacing-newline-characters-in-vb-net-strings.md new file mode 100644 index 0000000..6cfd871 --- /dev/null +++ b/src/posts/2009/09/01/replacing-newline-characters-in-vb-net-strings.md @@ -0,0 +1,20 @@ +--- +layout: post +title: "Replacing newline characters in VB.NET strings" +date: 2012-06-12 +description: "Replacing newline characters in VB.NET strings" +categories: [programming] +tags: [vb.net] +--- +I’m not very familiar (which I am actually very happy for!) with VB.NET and so I had a hard time figuring out how to trim newline characters from a string in a Reporting Services report I was working on. + +Turns out none of the “regular” methods really work. The Trim function only removes spaces and anything like Replace(“string”, “\n”, “”) didn’t work. + +You have to use the Chr(13) or Chr(10) functions to actually get the character values for the newline characters. So, my solution looked like this: + +{% highlight csharp %} +Replace(Replace(stringVariable, Chr(10), “”), Chr(13), “”) +{% endhighlight %} + +Thanks to this forum post for providing the clues: +[http://www.daniweb.com/forums/thread54797.html](http://www.daniweb.com/forums/thread54797.html) diff --git a/src/posts/2009/09/24/converting-a-hexadecimal-color-to-a-system-drawing-color-object.md b/src/posts/2009/09/24/converting-a-hexadecimal-color-to-a-system-drawing-color-object.md new file mode 100644 index 0000000..30fcb72 --- /dev/null +++ b/src/posts/2009/09/24/converting-a-hexadecimal-color-to-a-system-drawing-color-object.md @@ -0,0 +1,18 @@ +--- +layout: post +title: "Converting a Hexadecimal Color to a System.Drawing.Color Object" +date: 2009-09-24 +description: "Converting a Hexadecimal Color to a System.Drawing.Color Object" +categories: [programming] +tags: [.net] +--- +I try to avoid setting styles in my code as much as possible. However, I ran across a case using a DevExpress PageControl control where there was no possible way to set a certain border by using CSS. This is because DevExpress wisely (read sarcasm) uses “border-color: #whatever !important” in the element’s style attribute. So, there is no way to override that style except by setting it on the control in code (or your aspx page). + +The particular border I needed is the ContentStyle.Border border of the ASPxPageControl and I needed to set the BorderColor property which is a System.Drawing.Color object. There isn’t any straightforward way to get a Color object from a hexadecimal color using the Color object itself. You have to use the ColorTranslator (also in the System.Drawing namespace) which gives you a FromHtml method and returns the correct Color object. + +{% highlight csharp %} +Color hexColor = ColorTranslator.FromHtml(“#666666″); +{% endhighlight %} + +Thanks to this guy for the quick answer: +[http://www.akxl.net/labs/articles/converting-a-hexadecimal-color-to-a-system.drawing.color-object/](http://www.akxl.net/labs/articles/converting-a-hexadecimal-color-to-a-system.drawing.color-object/) diff --git a/src/posts/2010/06/29/vertical-text-with-css.md b/src/posts/2010/06/29/vertical-text-with-css.md new file mode 100644 index 0000000..b54ed08 --- /dev/null +++ b/src/posts/2010/06/29/vertical-text-with-css.md @@ -0,0 +1,13 @@ +--- +layout: post +title: "Vertical text with CSS" +date: 2010-06-29 +description: "Vertical text with CSS" +categories: [programming] +tags: [css] +--- +I found a nice blog post today describing how to do vertical text with CSS. Instead of reposting his information, I’ll just give the link. + +[http://scottgale.com/blog/css-vertical-text/2010/03/01/](http://scottgale.com/blog/css-vertical-text/2010/03/01/) + +It’s a nice technique using various proprietary methods since there is no standard way to get vertical text across the major browsers right now. diff --git a/src/posts/2010/06/29/visual-studio-post-build-event-to-copy-dlls.md b/src/posts/2010/06/29/visual-studio-post-build-event-to-copy-dlls.md new file mode 100644 index 0000000..5035ce2 --- /dev/null +++ b/src/posts/2010/06/29/visual-studio-post-build-event-to-copy-dlls.md @@ -0,0 +1,26 @@ +--- +layout: post +title: "Visual Studio Post Build Event to Copy DLLs" +date: 2010-06-29 +description: "Visual Studio Post Build Event to Copy DLLs" +categories: [programming] +tags: [visual-studio] +--- +While doing SharePoint development, we use a lot of pre- and post-build events in Visual Studio projects to do various things like build and deploy SharePoint solutions to our development environments, activate features automatically and other similar things to make the process of building solutions for SharePoint as automated as possible. + +One helpful way I’ve been able to use post-build events is to copy the compiled DLLs for a given project to the inetpub directory for the SharePoint site I am currently working on. As I’m building a new feature or debugging an existing one, I like to build a lot of see my changes as I go. Well, building and deploying the entire SharePoint solution is a rather lengthy process it’s only really necessary if you’re changing ASPX pages or updating a feature XML file or something like that. If all I did was come C# code changes, that’s too much to wait through to see my changes. So, what I do is put a small post-build event in my projects that I’m working with that copies the DLLs for that project to my SharePoint site’s inetpub directory and I skip the whole SharePoint solution build and deployment step. + +Adding a post-build event couldn’t be much simpler either. In Visual Studio just right-click on the project you want to add the post-build event for in Solution Explorer and select “Properties”. You can either do that or, with the project selected in Solution Explorer, hit Alt-Enter. The project properties window will display. On the left-hand side, click the “Build Events” tab. You’ll see two boxes there. One is for pre-build events and one is for post-build events. + +{% highlight text %} +In the post-build events textbox add the following: +IF NOT ($(ConfigurationName)) == (Debug) GOTO END +cd $(ProjectDir) +copy /y bin\debug\*.dll C:\inetpub\wwwroot\wss\VirtualDirectories\{YOUR SHAREPOINT SITE DIRECTORY}\bin +copy /y bin\debug\*.pdb C:\inetpub\wwwroot\wss\VirtualDirectories\{YOUR SHAREPOINT SITE DIRECTORY}\bin +:END +{% endhighlight %} + +These are just basic Windows batch commands like you’d find in any Windows batch file. This is a very basic setup, but it should be a good starting point for you to work from. All it does is check whether you are building with your Debug build configuration, if you are, then it moves to your project directory and copies the DLLs in your bin\debug folder to the bin folder of your SharePoint site. + +Easy enough! diff --git a/src/posts/2010/10/06/determine-sharepoint-moss-2007-service-pack-version.md b/src/posts/2010/10/06/determine-sharepoint-moss-2007-service-pack-version.md new file mode 100644 index 0000000..e908883 --- /dev/null +++ b/src/posts/2010/10/06/determine-sharepoint-moss-2007-service-pack-version.md @@ -0,0 +1,20 @@ +--- +layout: post +title: "Determine SharePoint MOSS 2007 Service Pack Version" +date: 2010-10-06 +description: "Determine SharePoint MOSS 2007 Service Pack Version" +categories: [programming] +tags: [sharepoint] +--- +Determining the service pack that you have installed for a particular +SharePoint 2007 installation turned out to be a surprisingly opaque process. +I found this very clear and helpful blog post dealing with just this +determination: +[http://techpunch.wordpress.com/2008/10/15/sharepoint-2007-moss-how-to-determine-service-pack-version/](http://techpunch.wordpress.com/2008/10/15/sharepoint-2007-moss-how-to-determine-service-pack-version/) + +To give a quick overview, the right way to determine the installed service pack is to look up the version of SharePoint that you have installed and match it against a table (available at the link above) of version numbers to service packs. + +Determine your SharePoint version by logging into your Central Administration site and going to Operations > Servers In Farm > Database Schema Version. Your SharePoint version will be listed in the table of servers in the farm. + +Note that the link above only has version numbers listed through SP1 (the post is from October 2008). Use this link to get a more up-to-date listing of version numbers: +[http://www.sharepointdesignerstepbystep.com/Blog/Articles/How%20To%20find%20the%20SharePoint%20version.aspx](http://www.sharepointdesignerstepbystep.com/Blog/Articles/How%20To%20find%20the%20SharePoint%20version.aspx) diff --git a/src/posts/2010/10/06/sharepoint-2007-to-2010-in-place-upgrade-servicehostingenvironment-error.md b/src/posts/2010/10/06/sharepoint-2007-to-2010-in-place-upgrade-servicehostingenvironment-error.md new file mode 100644 index 0000000..c113a1a --- /dev/null +++ b/src/posts/2010/10/06/sharepoint-2007-to-2010-in-place-upgrade-servicehostingenvironment-error.md @@ -0,0 +1,29 @@ +--- +layout: post +title: "SharePoint 2007 to 2010 in-place upgrade serviceHostingEnvironment error" +date: 2010-10-06 +description: "SharePoint 2007 to 2010 in-place upgrade serviceHostingEnvironment error" +categories: [programming] +tags: [sharepoint] +--- +I’m testing an in-place SharePoint 2007 to SharePoint 2010 upgrade today and I came across this odd error during the update process. + +I successfully installed SharePoint 2010 and proceeded to run the configuration wizard which updates 2007 to 2010. The configuration process would always fail with an error in the Central Administration web.config file. Viewing the configuration log file (the configuration wizard provided the location of it) turned up the error. + +Apparently, at some point in the process the configuration wizard looks for the serviceHostingEnvironment element under the `` section in the Central Administration web.config. + +{% highlight xml %} + +{% endhighlight %} + +If it doesn’t find it adds it itself, but curiously, it wraps it in an incomplete system.serviceModel tag so it looks like this: + +{% highlight xml %} + + + +{% endhighlight %} + +This then caused the configuration wizard to bomb out. + +The simple solution was to just add the `` element under my existing `` section. The wizard completed just fine after this. diff --git a/src/posts/2010/10/08/error-occurred-in-deployment-step-recycle-iis-application-pool-the-local-sharepoint-server-is-not-available-check-that-the-server-is-running-and-connected-to-the-sharepoint-farm.md b/src/posts/2010/10/08/error-occurred-in-deployment-step-recycle-iis-application-pool-the-local-sharepoint-server-is-not-available-check-that-the-server-is-running-and-connected-to-the-sharepoint-farm.md new file mode 100644 index 0000000..ca7f320 --- /dev/null +++ b/src/posts/2010/10/08/error-occurred-in-deployment-step-recycle-iis-application-pool-the-local-sharepoint-server-is-not-available-check-that-the-server-is-running-and-connected-to-the-sharepoint-farm.md @@ -0,0 +1,15 @@ +--- +layout: post +title: "Error occurred in deployment step ‘Recycle IIS Application Pool’: The local SharePoint server is not available. Check that the server is running and connected to the SharePoint farm." +date: 2010-10-08 +description: "Error occurred in deployment step ‘Recycle IIS Application Pool’: The local SharePoint server is not available. Check that the server is running and connected to the SharePoint farm." +categories: [programming] +tags: [sharepoint] +--- +I got the above error today while trying to deploy a SharePoint 2010 project in Visual Studio 2010. Very frustrating!! + +The answer turned out to be simple enough, though no clues are really provided in the error message (typical). I had to add my user account to the db_owner role for the SharePoint_Admin database that the site I was deploying to was using. + +Once I did that, the deployment in Visual Studio did just great. + +The following post led me to the solution: [http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/a97a8413-a0ae-47f2-aab7-fae1cf40595a](http://social.technet.microsoft.com/Forums/en-US/sharepoint2010programming/thread/a97a8413-a0ae-47f2-aab7-fae1cf40595a) diff --git a/src/posts/2010/10/12/cookie-expires-from-asp-net-request-object-always-datetime-minvalue.md b/src/posts/2010/10/12/cookie-expires-from-asp-net-request-object-always-datetime-minvalue.md new file mode 100644 index 0000000..c373db1 --- /dev/null +++ b/src/posts/2010/10/12/cookie-expires-from-asp-net-request-object-always-datetime-minvalue.md @@ -0,0 +1,13 @@ +--- +layout: post +title: "Cookie.Expires from ASP.NET Request Object Always DateTime.MinValue" +date: 2010-10-12 +description: "Cookie.Expires from ASP.NET Request Object Always DateTime.MinValue" +categories: [programming] +tags: [.net,asp.net] +--- +I’ve been working on a problem today involving cookies with one of our web applications. The cookie is keeping track of a user’s authenticated session and it’s timing people out too soon. So, I was checking the expiration time of the cookie coming in from the browser and it was returning 1/1/0001 12:00AM every time. I thought this might be the source of my problem, but resetting the cookie and the expiration time did not help the next round-trip to the server. The cookie still read the DateTime.MinValue time. + +I found this article: [http://www.eggheadcafe.com/tutorials/aspnet/198ce250-59da-4388-89e5-fce33d725aa7/aspnet-cookies-faq.aspx](http://www.eggheadcafe.com/tutorials/aspnet/198ce250-59da-4388-89e5-fce33d725aa7/aspnet-cookies-faq.aspx) + +The articles explains why this happens. The summary: the browser maintains the cookie and handles its expiration. Therefore, it does not send this information back to the server when making a request. So, you can never read the expiration time of a cookie on the server from the Request object. diff --git a/src/posts/2010/11/02/add-sharepoint-2010-central-admin-web-applications-page-ribbon-button.md b/src/posts/2010/11/02/add-sharepoint-2010-central-admin-web-applications-page-ribbon-button.md new file mode 100644 index 0000000..06359a0 --- /dev/null +++ b/src/posts/2010/11/02/add-sharepoint-2010-central-admin-web-applications-page-ribbon-button.md @@ -0,0 +1,17 @@ +--- +layout: post +title: "Add SharePoint 2010 Central Admin Web Applications Page Ribbon Button" +date: 2010-11-02 +description: "Add SharePoint 2010 Central Admin Web Applications Page Ribbon Button" +categories: [programming] +tags: [sharepoint] +--- +I finally figured out today how to add a button to the Central Admin site’s ribbon for the WebApplicationsList page and have it behave like the other ribbon buttons on this page. The problem wasn’t so much adding the button–that part was easy. The problem was getting the button to behave like the other buttons. That is, getting it to enable and disable when a web application was selected from the list. + +It was pretty easy to find the methods for the new SharePoint 2010 javascript API to see if a list item is selected in a list and then make decisions based on that. The SP.UI.ListOperation.Selection methods are great for that. However, the WebApplicationList (http:///_admin/WebApplicationList.aspx) page list in the Central Administration site is not a list and so these methods don’t work. + +I had to do some digging in the SharePoint 2010 javacript files to find what I was looking for. I found what I was looking for in the C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\SP.UI.Admin.debug.js file. This is the debug version of the SP.UI.Admin.js file (so it’s possible to read it) that gets included on these pages. It includes a set of methods beginning with “SP.UI.Admin.WebApplicationPageComponent” that are used for the WebApplicationsList page. + +What you want is the SP.UI.Admin.WebApplicationPageComponent.get_selectedItem method. This method returns an object containing information about the selected web application on the page. + +Here is my final CustomAction XML for the button that enables the button when a web application is selected from the list and then opens up a dialog window when the button is clicked, passing the web application ID to the page that opens in the dialog box. diff --git a/src/posts/2010/12/01/blank-sharepoint-site-problem.md b/src/posts/2010/12/01/blank-sharepoint-site-problem.md new file mode 100644 index 0000000..f08f038 --- /dev/null +++ b/src/posts/2010/12/01/blank-sharepoint-site-problem.md @@ -0,0 +1,13 @@ +--- +layout: post +title: "Blank SharePoint Site Problem" +date: 2011-12-01 +description: "Blank SharePoint Site Problem" +categories: [programming] +tags: [sharepoint] +--- +## Problem +A SharePoint site renders completely blank in the browser. It shows up immediately without any “processing” time and when looking at the HTML source for the page, it is completely blank–no HTML tags whatsoever. + +## Solution +In my case, the web.config file for the SharePoint application had duplicate entries in the system.webServer/handlers and system.webServer/modules elements. Once I removed those, the site rendered as expected. diff --git a/src/posts/2010/12/10/duplicating-spwebconfigmodifications.md b/src/posts/2010/12/10/duplicating-spwebconfigmodifications.md new file mode 100644 index 0000000..fc454d0 --- /dev/null +++ b/src/posts/2010/12/10/duplicating-spwebconfigmodifications.md @@ -0,0 +1,17 @@ +--- +layout: post +title: "Duplicating SPWebConfigModifications" +date: 2010-12-10 +description: "Duplicating SPWebConfigModifications" +categories: [programming] +tags: [sharepoint] +--- +## Problem +Despite everything looking OK (no duplicate SPWebConfigModifications for your web application), your SPWebConfigModifications are adding duplicate elements to your web.config file. + +## Solution (at least one possible one) +I ran into this problem today and discovered that in the Name property of the SPWebConfigModification I was not wrapping an attribute value in single quotes. Since the actual Value property of the SPWebConfigModification had quotes around the attribute value, whenever the SPWebConfigModification was applied, it could not find the existing element and therefore would add a second, duplicate element. + +In other words, this is wrong for the SPWebConfigModification Name: “add[@name=valueOfAttribute]“. + +This is correct: “add[@name=’valueOfAttribute’]“ diff --git a/src/posts/2011/04/25/opening-all-sharepoint-2010-documents-in-a-new-window.md b/src/posts/2011/04/25/opening-all-sharepoint-2010-documents-in-a-new-window.md new file mode 100644 index 0000000..eaa193f --- /dev/null +++ b/src/posts/2011/04/25/opening-all-sharepoint-2010-documents-in-a-new-window.md @@ -0,0 +1,52 @@ +--- +layout: post +title: "Opening all SharePoint 2010 Documents in a new window" +date: 2011-04-25 +description: "Opening all SharePoint 2010 Documents in a new window" +categories: [programming] +tags: [sharepoint,javascript] +--- +We wanted an easy solution to open every document in a SharePoint from a document library in a new window. This way, the user stays on the page he is originally on and the document (PDF, Word doc, Excel files–whatever) will open in a blank window. + +I don’t know of any way to do this via a setting in SharePoint, but a little JavaScript employing jQuery does the job just perfectly. + +{% highlight javascript %} +$(document).ready( + function () { + // has to be on an interval for grouped doc libraries + // where the actual links are loaded only once a group + // is expanded + setInterval( + function () { + $("a[onclick*=’return DispEx’][target!=’_blank’]") + .attr("target", "_blank") + .removeAttr("onclick"); + + // document type icons + $("td.ms-vb-icon>img[onclick]:not([documentUrl])") + .click(function (e) { + window.open($(this).attr("documentUrl"), "_blank"); + e.stopPropagation(); + e.preventDefault(); + return false; + }) + .each(function () { + $(this).attr( + "documentUrl", + $.trim(String($(this).attr("onclick")) + .split("=")[1] + .replace(/["'{}]/g, "") + .split(";")[0]) + ); + this.onclick = null; + }); + }, + 500 + ); + } +); +{% endhighlight %} + +This JavaScript takes into account that some Document Libraries are grouped and so not all the document links (or icon links) will appear on the page load. This is the reason for the half-second interval. At the most half a second after the group is expanded the newly loaded links will be altered to also open in a new window. + +You’ll notice that there are two things happening on each interval. The first line does any Title links for the document. The second line (and following) handles the document type icons that are also links to the document. Note that if you want only one or the other link to open in a new window (for example, on the document type icon opens in a new window and the Title link opens in the same window) then all you need to do is remove the appropriate line. diff --git a/src/posts/2011/08/24/file-name-sanitizer-one-liner.md b/src/posts/2011/08/24/file-name-sanitizer-one-liner.md new file mode 100644 index 0000000..1bcef1b --- /dev/null +++ b/src/posts/2011/08/24/file-name-sanitizer-one-liner.md @@ -0,0 +1,19 @@ +--- +layout: post +title: "File Name Sanitizer One-Liner" +date: 2011-08-24 +description: "File Name Sanitizer One-Liner" +categories: [programming] +tags: [command-line] +--- +I find myself often having to sanitize file names and paths because I’m creating files from some kind of user input that I’m unsure about. I found this handy way of doing this in one line today: + +{% highlight text %} +fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (name, c) => name.Replace(c, ‘_’)); +{% endhighlight %} + +Note that the same goes for paths. The only thing that changes is the static method you use from Path: + +{% highlight text %} +filePath = Path.GetInvalidPathChars().Aggregate(filePath , (name, c) => name.Replace(c, ‘_’)); +{% endhighlight %} diff --git a/src/posts/2011/11/14/ansiutf-8-encoding-problem.md b/src/posts/2011/11/14/ansiutf-8-encoding-problem.md new file mode 100644 index 0000000..1b6a3cc --- /dev/null +++ b/src/posts/2011/11/14/ansiutf-8-encoding-problem.md @@ -0,0 +1,34 @@ +--- +layout: post +title: "ANSI/UTF-8 Encoding Problem" +date: 2011-11-14 +description: "ANSI/UTF-8 Encoding Problem" +categories: [programming] +tags: [] +--- +## Problem +I ran into a problem in a project I was working on recently. The basics of the project were to take data out of a MySQL database and transform that data into an XML file schema for a +[Day CQ5 CMS](http://day.com/day/en/products.html) +[Java Content Repository (JCR)](http://www.ibm.com/developerworks/java/library/j-jcr/). + +I kept running into an issue with certain text fields of user-entered data where I would get all these funky, special characters in the text. I knew it had to be some kind of encoding issue where I wasn’t outputting or getting the data with the right character encoding. + +I don’t know much about character encoding, except that there is a difference between them! + +## Solution +After some digging around and playing with Notepad++ (where I could switch the encoding of the text), I found at least a glimmer of hope. When I had the character encoding set to ANSI in Notepad++, pasted in the troublesome text, and then switched the encoding to UTF-8, all of my problems disappeared. + +So, eventually, I came up with these few lines of code which convert the encoding of my text and fixed all of my problems. Woohoo! + +{% highlight csharp %} +Encoding targetEncoding = Encoding.GetEncoding(1252); +byte[] utf8Bytes = targetEncoding.GetBytes(text); +byte[] ansiBytes = Encoding.Convert(Encoding.UTF8, +targetEncoding, +utf8Bytes); +return targetEncoding.GetString(ansiBytes); +{% endhighlight %} + +What this simple code does is first get the Encoding for the ANSI character encoding (1252 is ANSI, apparently). It then gets the bytes of the string with the ANSI encoding, converts them from the ANSI encoding to UTF-8 encoding and then gets the string with the ANSI encoding. + +So, quite honestly, I’m not 100% sure why this works. The Notepad++ experiment would seem to indicate the opposite: that I had to convert from ANSI to UTF-8. But, that only compounded the problem when I wrote it that way in code. When I reversed the conversion, it worked as I was hoping. diff --git a/src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md b/src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md new file mode 100644 index 0000000..d095d04 --- /dev/null +++ b/src/posts/2011/11/21/jquery-tools-overlay-v1-2-5-bug.md @@ -0,0 +1,21 @@ +--- +layout: post +title: "jQuery Tools Overlay v1.2.5 Bug" +date: 2011-11-21 +description: "jQuery Tools Overlay v1.2.5 Bug" +categories: [programming] +tags: [jquery] +--- +## Problem +I noticed a bug in the jQuery Tools library today, specifically in version 1.2.5 and the Overlay plugin. I should say, I’m assuming it’s a bug. This is an existing website I am working on where I am not in a position to upgrade the jQuery Tools library to the latest version (1.2.6 is out, it looks like) and I don’t know how much of the problem may be due to how existing code is possibly interfering with the library. + +In any case, the bug showed up when I had an existing overlay showing and wanted to show another overlay. By default, the library allows one overlay at a time. This was fine with me as I needed to close the existing overlay before showing the new one. The library handles this automatically. However, when the second overlay was displayed, the mask would not display and the z-index was not set, so it showed up behind other content. + +## Solution +While it’s somewhat of a hack, the best solution I found was to simply remove the div that Overlay creates with id “exposeMask” before I display the second overlay. + +{% highlight text %} +$(“#exposeMask”).remove(); +{% endhighlight %} + +I believe this is what is happening: when the first overlay is closed by the library, it doesn’t properly handle the existing exposeMask div and so when the second overlay shows it misunderstands the mask to be displaying properly and doesn’t calculate its own display values properly. Therefore, its z-index is wrong (or non-existing) and the mask remains hidden (because the library hid it when it closed the first overlay). diff --git a/src/posts/2011/12/14/formatting-multiple-xml-files-at-a-time.md b/src/posts/2011/12/14/formatting-multiple-xml-files-at-a-time.md new file mode 100644 index 0000000..b15c35b --- /dev/null +++ b/src/posts/2011/12/14/formatting-multiple-xml-files-at-a-time.md @@ -0,0 +1,34 @@ +--- +layout: post +title: "Formatting multiple XML files at a time" +date: 2011-12-14 +description: "Formatting multiple XML files at a time" +categories: [programming] +tags: [xml] +--- +I recently had a situation where I needed to compare many XML files generated by a program of one version to the same set of XML files produced by a previous version of the same program. Unfortunately, the sets of XML files were formatted differently and so doing a file comparison with Beyond Compare (a GREAT file comparison tool, by the way) was going to be useless. + +So, I started looking for a way to quickly format all the files in each set the same way with one program. I looked into using Notepad++ which has a great XML Tools plugin (look for it under Plugins > Plugin Manager). I tried combining the plugin’s formatting commands with a macro that would format the XML file, save it and close it. So, I could easily open the few hundred files I had to format in Notepad++ (one set at a time), then run the macro multiple times (Macro > Run a Macro Multiple Times…). This would run through each file until all were formatted and closed. However, after working with it for a while, I couldn’t get the Notepad++ macro system to actual perform the XML Tools plugin format command. The macro would successfully run, saving and closing the file. But, when I checked the files they had not been formatted. I worked with it for a while, but could not figure out what the matter was. + +I knew the real solution had to be some type of command-line utility and a batch file. So, I started looking into that. The solution I ended up with was just that. + +First of all, I found HTML Tidy which I could run from the Windows command line to format a file. Using a configuration file for the tidy.exe (placed in the same directory as tidy.exe and named tidcfg.ini–although neither matters, see below) that looked like this: + +{% highlight text %} +indent:yes +indent-attributes:yes +{% endhighlight %} + +I got the formatting I wanted. + +Now, all I had to do was brush up on my Windows batch command skills to run tidy.exe on multiple files. Easy enough! This is what my batch file looked like: + +{% highlight text %} +for /d %%X in (C:\\*) do (c:\\tidy.exe -m -xml -config c:\\tidycfg.ini %%X\.xml) +{% endhighlight %} + +I had a folder structure where there were hundreds of directories inside this one parent directory. Each of the child directories had a single XML file in it. Therefore, I needed the C:\\* wildcard. + +So, what this batch file does is simply look at each child directory (with the /d switch) in my parent directory. In each directory it runs (do) the tidy.exe program, tells it to modify the input file itself (-m) instead of saving the formatted XML to another file, tells it that the input file is valid XML (-xml) and then tells it where the tidycfg.ini file is (-config). Finally, it tells tidy.exe to take the current directory (%%X) and use the .xml file as the input file to format. + +This little set up worked very well and quickly formatted all of my files in the same way so that I could successfully compare them with Beyond Compare. diff --git a/src/posts/2012/02/03/how-to-create-a-brightcove-player-logo-overlay.md b/src/posts/2012/02/03/how-to-create-a-brightcove-player-logo-overlay.md new file mode 100644 index 0000000..45c0df8 --- /dev/null +++ b/src/posts/2012/02/03/how-to-create-a-brightcove-player-logo-overlay.md @@ -0,0 +1,56 @@ +--- +layout: post +title: "How to Create a Brightcove Player Logo Overlay" +date: 2012-02-03 +description: "How to Create a Brightcove Player Logo Overlay" +categories: [programming] +tags: [brightcove] +--- +As simple as it ended up being, it took me quite a while to figure out how to correctly add a simple overlaying graphic to a Brightcove video player yesterday. It involves a couple of steps which I did not find clearly laid out anywhere I looked. So, I thought a nice little step-by-step blog post would be helpful to anyone else looking for the same thing. + +First, Brightcove has a nice and easy method (I say that because it looks easy…I didn’t try this method) of adding logo overlays to an individual video. You just: + +1. Log into your video cloud Brightcove Studio +1. Click on the Media icon in the top navigation bar +1. Find and select your video (by clicking on it) +1. Click on the Edit button at the bottom of the list of videos +1. Follow the process outlined on the Logo Overlay tab of the Edit popup + +This isn’t what I wanted though. I needed these videos to appear logo-free in certain instances. So, the best solution was to use one player in the instance where I didn’t need a logo and then use a different player that included the logo overlay for my other scenario. That required add an overlay to a player, not a video. + +So, on to the solution! + +## Step One: Create a new player template +This part is easy. Actually, all the parts are easy. You just need to know what to do. + +1. Under the Publishing tab in your Brightcove Studio, click on All Templates in the left sidebar. +1. Select one of the templates that Brightcove gives you and duplicate it (use the Duplicate button at the bottom of the list of templates) +1. Select your new template and click on the Edit button +1. In the Layout field (the XML that describes the player), add an Image element inside the Layout element. See the BEML reference documentationfor details. Here is what my Image element looks like: + +{% highlight xml %} + +{% endhighlight %} + +All of the attributes are optional except the id attribute. It doesn’t have to be named “logoOverlay”, but you have to have something there. Notice the value for my “visible” attribute. This ensures that my logo disappears when the video is done and doesn’t awkwardly overlay any of the video menu UI elements. Remove it if you want your logo to display the whole time. Also note the “x” and “y” values. Change these to your liking. You might have to wait to finish Step Two below when you’ve added a logo and then you can test a video with your player to see where the logo fits best. From there, you can keep adjusting your logo until you’re satisfied. +1. Now click on All Players in the left sidebar and then click on the New Player button at the bottom +1. Create a new player with whatever name is appropriate from the template you just created + +That’s it for the template! You’re done with Step One. + +## Step Two: Add Your Logo +I figured out step one all on my own pretty quickly. I figured out step two all on my own too, but it look a lot longer! + +1. Under the Publishing tab (you’re probably already there), click on All Players in the left sidebar +1. Select the player you just created in Step One and click on the Styles button at the bottom of the page. +1. If you gave your Image element an id (you should have!), then you should see an item underneath Editable Areas in the right sidebar with the same name as your Image id. Click on it. +1. Just below (also in the right sidebar), you should now see an Image Selection tab in the Edit pane +1. Click on Upload and upload your logo (or whatever image). +1. Add a URL that the image links to and a tooltip to show when you hover over the image, if you want +1. Click on Save & Close + +You’re done! Now, any video you display with that player will have a logo overlay. diff --git a/src/posts/2012/02/03/install-sharepoint-on-windows-7.md b/src/posts/2012/02/03/install-sharepoint-on-windows-7.md new file mode 100644 index 0000000..0268600 --- /dev/null +++ b/src/posts/2012/02/03/install-sharepoint-on-windows-7.md @@ -0,0 +1,21 @@ +--- +layout: post +title: "Install SharePoint on Windows 7" +date: 2012-02-03 +description: "Install SharePoint on Windows 7" +categories: [programming] +tags: [sharepoint] +--- +If you try to do a SharePoint 2010 installation on your client machine (that is, not a Windows Server OS) then you’ll get an error explaining that SharePoint 2010 can’t be installed on your operating system. + +Never fear, CodeProject is here. Check out this helpful post to get it installed (it couldn’t be easier): + +[CodeProject article](http://www.codeproject.com/Articles/44210/Installing-SharePoint-Server-2010-on-Windows-7-x64) + +Even though the post is from 2009, it still works great. + +For more detailed steps (including the ones outlined in the link above) following these instructions: + +[Microsoft’s detailed steps](http://msdn.microsoft.com/en-us/library/ie/ee554869.aspx) + +They will help you get the prerequisites installed as well. diff --git a/src/posts/2012/02/27/ektron-license-key-update.md b/src/posts/2012/02/27/ektron-license-key-update.md new file mode 100644 index 0000000..e07d156 --- /dev/null +++ b/src/posts/2012/02/27/ektron-license-key-update.md @@ -0,0 +1,19 @@ +--- +layout: post +title: "Ektron License Key Update" +date: 2012-02-27 +description: "Ektron License Key Update" +categories: [programming] +tags: [ektron] +--- +## Problem +We use a temporary license key with our Ektron development website here at work. Because it’s temporary, we have to update it every-so-often. That happened today. + +I got the new key and copy-pasted it into the right License Key field under Configuration / Settings. I clicked on “Save” and thought I was done. When I tried to log back in with my regular account (the way you are notified of the license expiration is a license violation error when you try log in) and it still gave me the license violation error. So, I logged back in with the built-in account (which lets you log in with limited features so that you can update the license key) and found out that I had copy-pasted the license key wrong. It was missing part of the domain name. + +Naturally, I tried simply updating the license key domain name, but Ektron threw a Javascript error which prevented me from saving the page saying that I had updated the license key in a way that would make it invalid. What?! I tried re-pasting the key in, thinking I had it wrong still. I didn’t work. + +## Solution +Turns out this has to be an Ektron bug. I ended up trying something random: I changed the last number of the license key to something arbitrary (along with making my domain name change) and it updated fine. Strange, I know…but this IS Ektron. + +Of course, that is an invalid key, so once the page saved, I changed that last digit back to the original digit from the valid license key and it let me save it and I could log in again. diff --git a/src/posts/2012/03/05/grouping-into-rows-with-xslt.md b/src/posts/2012/03/05/grouping-into-rows-with-xslt.md new file mode 100644 index 0000000..b07efff --- /dev/null +++ b/src/posts/2012/03/05/grouping-into-rows-with-xslt.md @@ -0,0 +1,168 @@ +--- +layout: post +title: "Grouping into Rows with XSLT" +date: 2012-03-05 +description: "Grouping into Rows with XSLT" +categories: [programming] +tags: [xslt] +--- +Let’s say you have a simple XML document that looks something like this: + +{% highlight xml %} + + + + + + +{% endhighlight %} + +The content or meaning of the pods is irrelevant. The idea is that you have a “list” of elements (pods in this case) in your XML. How, then, do you use XSLT to group these pods into rows of two, three or whatever number of pods across? Something like this: + +{% highlight xml %} +
+
+
pod 1
+
pod 2
+
pod 3
+
+
+
pod 4
+
+
+{% endhighlight %} + +This example would required three pods per row. The example XSLT below will work for any number of pods per row with a simple edit (explained below). + +My first thought was from a very normal programmer’s perspective. I’d use some kind of looping (with xsl:for-each) and just start a new row for each x number of pods. So, I did it that way and it worked. It was pretty ugly XSLT, but I didn’t really think there was a better way to do it. But then a colleague looked over my shoulder and chuckled. “I did it that way at first too”, he said, “but that’s not the XSLT way to do it.” He gave me a little bit of a clue of how it really should be done then he left, leaving me determined to find the “XSLT way” of doing it! + +Turns out, it really isn’t difficult. You just have to think a little differently and start using xsl:template, xsl:call-template and xsl:apply-templates effectively along with their select and match attributes. So, take a look at this XSLT file: + +{% highlight xml %} + + + + + + + + + +
+ + + + +
+
+ + +
+ +
+
+ + + + +
+{% endhighlight %} + +Here’s what’s going on: first, we call xsl:apply-templates on the first and then every third Pod using “Pod[position() mod 3 = 1]”. This will select the Pod at the start of every row: Pods 1 and 4. Notice that XSLT indexing starts at 1 and NOT 0. The PodRow template will match these selected Pods. + +Inside the PodRow template we have the HTML for the row. Inside the row HTML, we explicitly call the PodWrapper template for the current Pod. We then call xsl:apply-templates on the following siblings of the current Pod, but only on the next two of them. This grabs the remaining Pods in the row. This xsl:apply-templates select will match the PodWrapper template which creates the HTML for a single Pod. + +Easy as pie! To change the number of pods in a row, update all of the “position() mod 3″s by replacing 3 with however many pods you want in a row. Then also make sure to update the xsl:apply-templates select attribute inside PodRow to be “position() < n-1″ where n is the number of Pods in a row. + +## Identifying the last cell of every row +I needed another feature from my XSLT though. I needed every last pod in each row to have a special CSS class. This meant determining which pod was the last one in each row whether the row was full or not. This made the XSLT significantly more complicated, but not overwhelmingly so. + +I needed my HTML to look like this (notice the additional “last” class): + +{% highlight xml %} +
+
+
pod 1
+
pod 2
+
pod 3
+
+
+
pod 4
+
+
+{% endhighlight %} + +So, this is what I had to do: + +{% highlight xml %} + + + + + + + + + +
+ + + + + + + + + +
+
+ + +
+ +
+
+ + +
+ +
+
+ + + + +
+{% endhighlight %} + +This version of the XSLT functions exactly the same as the previous one. The only additions are for identifying the last pod in each row for all cases. So, here is what has changed. + +The xsl:apply-templates call inside of PodRow now matches one of two templates: PodWrapper or PodWrapperLast. You guessed it, PodWrapperLast just has an extra “last” class on the Pod div and a fancy match attribute. “Pod[position() mod 3 = 0 or position() = last()]” will match Pod numbers 3, 6, etc (in other words, the last Pod of every row) or the last Pod in the XML (no matter what number it is). + +Notice the new xsl:choose inside of our PodRow template. This handles the case where the last Pod in the XML is also the first Pod in a row. + +Notice also the “priority” attributes on the templates now. This prevents the PodWrapperLast template from matching the first Pod in a row if that first Pod is also the last Pod in the XML. diff --git a/src/posts/2012/03/07/easy-way-to-make-huge-maps-with-google-maps.md b/src/posts/2012/03/07/easy-way-to-make-huge-maps-with-google-maps.md new file mode 100644 index 0000000..81a6a7f --- /dev/null +++ b/src/posts/2012/03/07/easy-way-to-make-huge-maps-with-google-maps.md @@ -0,0 +1,10 @@ +--- +layout: post +title: "Easy Way to Make Huge Maps with Google Maps" +date: 2012-03-07 +description: "Easy Way to Make Huge Maps with Google Maps" +categories: [programming] +tags: [google-maps] +--- + +[Easy way to make huge maps with Google Maps](http://www.metacafe.com/watch/1073912/google_maps_hack_how_to_save_large_maps/) diff --git a/src/posts/2012/03/15/change-the-color-of-an-icon-with-gimp.md b/src/posts/2012/03/15/change-the-color-of-an-icon-with-gimp.md new file mode 100644 index 0000000..fba3d00 --- /dev/null +++ b/src/posts/2012/03/15/change-the-color-of-an-icon-with-gimp.md @@ -0,0 +1,69 @@ +--- +layout: post +title: "Change the color of an icon with Gimp" +date: 2012-03-15 +description: "Change the color of an icon with Gimp" +categories: [programming] +tags: [gimp] +--- +Fairly often, I run across the perfect icon that I need for a website, but it’s just the wrong color. I want to keep the nice blending to the background of the icon around the edges, the shape and everything that makes the icon look spiffy. I just want the main color to be something else. + +It took me a long time to figure out a good method (I’m no graphic designer!), but I eventually came a cross a really easy method that works well using trusty Gimp. I’ll walk you through it. + +## 1. Find an Icon + +The first step, of course, is to find an icon. There are billions of free ones out there on the web. This is mine. + +![Blue phone icon](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/icon-phone-blue.png) + +I love it. It’s just what I need–but what a terrible color! I need something else. It’s a PNG and when I open it in Gimp, here’s what it looks like. + +![Original in Gimp](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/original_in_gimp-300x236.png) + +You can see the edges have been blended into a white background which would work great if I only ever put it on a white background. Well, I just might be doing that, but I’m too much of a perfectionist to use it like that. So, let’s get to work. + +## 2. Create a color layer + +Add a new layer to the image by selecting Layer > New Layer. Call it “color”. Make sure it’s on top of the “Background” layer. + +![Adding a color layer](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/color_layer-149x300.png) + +Fill this new layer with the color you want to change the icon to. I’m using this lovely puke color (not my choice!): #afbd0a. + +Now, hide this new layer so we can work on the “Background” layer by clicking on the eye button just to the left of the layer thumbnail. + +## 3. Manipulate the icon colors + +Select the “Background” layer. Desaturate the image by selecting Colors > Desaturate. I choose “Lightness” from the dialog box that pops up. + +Now, invert the image by selecting Colors > Invert. Next, create another layer called “mask” and fill it with black. Move this layer below the “Background” layer. Your layers should look like the image below. + +![Creating the mask layer](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/mask_layer-209x300.png) + +Right-click on the “Background” layer and select “Merge down”. Now you should have something similar to the image below. + +![After merging down](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/after_merge_down-300x156.png) + +## 4. Adjust the curves + +Now, with the “mask” layer selected, select Colors > Curves. In the dialog, drag the very middle of the line straight up to the top-center of the graph. Gimp should look like the screenshot below. Click “OK”. + +![Adjusting the curves](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/curves-300x189.png) + +## 5. Create a layer mask on the “color” layer + +Now, with the “mask” layer selected, copy the entire layer by selecting Select > All. Then select Edit > Copy. + +Next, right-click on the “color” layer and select “Add layer mask”. Choose “Black (full transparency)” and make sure “Invert mask” is checked. + +Paste what you’ve copied to the layer mask by selecting Edit > Paste. You’ll notice a new layer called “Floating selection (Pasted layer)”. Right-click on this new layer and select “Anchor layer”. The pasted layer will now be merged with your anchor layer. + +## 6. You’re done! + +Now hide your “mask” layer and show your “color” layer. Perfect! + +![Completed image](/assets/images/2012-03-15-change-the-color-of-an-icon-with-gimp/done-300x163.png) + +You now have a new icon with a completely different color, but with the nice blending at the edges and perfect shape of the original icon. Don’t believe me? Well, test it! Select your “mask” layer and delete everything on it. Fill it with any background color you want and see how it looks. Make sure to show your “mask” layer again by click on the eye button. + +Hope this was helpful! You can apply your layer mask on the “color” layer if you want. But, I like to keep it as it is and save the file as an xcf for later use. This way, if you ever need the icon in another color, all you have to do is change the fill color of the “color” layer and you’re done. diff --git a/src/posts/2012/06/12/limit-selected-items-on-sitecore-multilist-field.md b/src/posts/2012/06/12/limit-selected-items-on-sitecore-multilist-field.md new file mode 100644 index 0000000..baa5f18 --- /dev/null +++ b/src/posts/2012/06/12/limit-selected-items-on-sitecore-multilist-field.md @@ -0,0 +1,22 @@ +--- +layout: post +title: "Limit Selected Items on Sitecore Multilist Field" +date: 2012-06-12 +description: "Limit Selected Items on Sitecore Multilist Field" +categories: [programming] +tags: [sitecore] +--- +## Problem +You want to limit the number of items a user can choose for a multilist field in a data template in sitecore. + +## Solution +The solution is easy enough. Just add some validation to the field that will “count” the number of GUIDs in the raw value of the MultiList field. + +1. Open up the template in the tree navigation and click on the MultiList field you want to limit +![sitecore_template_property_nav](/assets/images/2012-06-12-limit-selected-items-on-sitecore-multilist-field/sitecore_template_property_nav.png) + +2. Scroll down to the Validation field and enter this: ^(\{[^}]+\}\|?){0,5}$. This regular expression simply makes sure you have between 0 and 5 GUIDs in the value of the MultiList, effectively limiting the number of items you select in the list. The {0,5} is the range. So, change the 0 to limit the lower bound and change the 5 to limit the upper bound. +![sitecore_template_validation_field](/assets/images/2012-06-12-limit-selected-items-on-sitecore-multilist-field/sitecore_template_validation_field.png) + +3. Enter an appropriate message that explains why the validation failed in the ValidationText field. +![sitecore_template_validationtext_field](/assets/images/2012-06-12-limit-selected-items-on-sitecore-multilist-field/sitecore_template_validationtext_field.png) diff --git a/src/posts/2012/07/19/grace-and-me.md b/src/posts/2012/07/19/grace-and-me.md new file mode 100644 index 0000000..1094f50 --- /dev/null +++ b/src/posts/2012/07/19/grace-and-me.md @@ -0,0 +1,43 @@ +--- +layout: post +title: "Grace & Me" +date: 2012-07-19 +description: "Grace & Me" +categories: [personal] +tags: [] +--- +Our story really begins somewhere around the beginning of 2007. That’s when Grace’s brother Nathan came down to Kansas City for a couple of Bible conferences that I (Ben) also happened to go to. I barely remember meeting Nate, but he must have made an impression because we started to become good friends soon afterward. On a whim one time as Nate and I were chatting on Facebook, he invited me up to a regular men’s Bible study in Colfax, WI led by Warren Henderson. That’s eight hours away from Kansas City and so Nate never thought I’d actually say yes. But, what Nate did not know was this was a tremendous answer to prayer for me. I’d been praying that the Lord would bring some good, solid young men who were my age into my life. This seemed like the perfect place to see that prayer answered. Little did I know that it would some day lead to me marrying Nate’s sister! + +Grace and I don’t really know for sure when we first met. We have our first, real evidence of being in the same place at the same time at the 2008 Vessels of Honor conference in Parkville, MO. But, it was probably a little before that, on a Sunday morning in Chippewa Falls, WI after one of those men’s Bible studies at the River Valley Christian Fellowship where Nathan, Grace and their family went to church. + +It’s pretty safe to say that neither Grace nor I really took too much notice of each other back then! I was just some friend of Nathan’s, driving up ridiculous distances for a one-day Bible study, and she was just Nathan’s younger sister. + +As time went on, Nathan got me involved in Story Book Lodge Bible Camp in Gilbert, MN. That’s even further north–almost as far north as you can get in Minnesota. Counseling at camp in the summer and attending Winter Retreat up at camp became a yearly event. Grace and I slowly got to know each other better as we attended the same camps and conferences over the next four years. + +Still, while we gradually became friends, as late as June of 2011 we would never have thought anything beyond friendship was in our future. + +As the summer of 2011 rolled along, Grace and I were both having some gracious and immensely valuable lessons from the Lord sink into our minds and hearts. Through completely separate, but similar situations we were both learning what trusting in the true goodness of our Lord meant. It was clear to both of us that we sure wanted to get married. But, who would it be to? When would it be? Would it ever happen? We had to trust the Lord that if He is good (and He is), then being single our whole lives would be the very best life we could imagine. If, on the other hand, being married some day was what the Lord had in store for us, the timing He had planned would be the only right timing there was. + +Then one day in the middle of that summer, Grace came to my mind. At the same time, I was nowhere near Grace’s mind! I was planning on counseling at Story Book Lodge’s 2nd Teen camp week and hoping Grace would be there. I was hoping I could just get to know her better. I prayed a lot about it. I asked the Lord that He would make things clear if I should take the next step after this week at camp. Amazingly, others were secretly praying at this same time specifically for me and Grace too–I just didn’t know about it! + +Meanwhile, Grace wasn’t even sure if she would make it to camp that year. It wasn’t clear if she could get off of work in time. But, at the last minute, she found out it was going to work and she was able to counsel that same week. + +I spent the week concentrating on the spiritual needs of my campers, but also taking what opportunities presented themselves (or that I could generate!) to be around Grace and talk with her. She, by her own admission, was completely oblivious to this while I was taking each smile or glance as a sure sign that she felt the same way about me that I was beginning to feel about her. + +The week was full of great happenings: exciting conversations with Grace, forceful proddings from her brother-in-law Cory, knowing looks from her sister Beth and finally suspicion on Grace’s part too. Then, the week came to a close. Camp is often a perception altering vortex of emotion. So, I wanted to be sure what I was thinking and feeling wasn’t a product of the strange air at Story Book (they don’t call it that for nothing, you know). + +So, I waited a week after camp to just think and pray. Meanwhile, Grace was wondering if her own suspicions were part of the Story Book vortex. She was praying for clear thinking about it too. + +The following Sunday though, I got up the courage to call her Dad and ask his permission to start pursuing Grace. I was nervous, but he gladly said yes and I called Grace a few minutes later. Thankfully, she said yes too! + +What followed were some of the happiest months of our lives so far. We traveled to Eau Claire or Kansas City as much as we could, trying to spend as much time together as possible. It seemed like things just went from good to better. We agreed on just about everything we could think to talk about and slowly but surely we fell rather deeply in love with each other. + +As February of 2012 rolled around, I knew I wanted to marry this girl. I bought a ring and made my plan to visit the Hanson’s over a week from the 17th to the 26th while the newly engaged David and Rachel (now) Hanson were visiting from California. A fishing trip was planned for that first Saturday which in Wisconsin means you’re getting up at 4am. But, I couldn’t wait any longer. I asked Joel (Grace’s Dad) if I could talk to him that morning before we left for fishing and he seemed to know what might be coming. So, we both got up around 3:30 and I asked him if I could marry his daughter. Yes, was the answer! Now I had a little more planning to do. + +I found a fire tower not too far away where Grace and I could climb up and watch the sunrise together. I imagined a beautiful morning, no clouds and a little nice, wooden room on top of the fire tower where I could propose and we could eat a little breakfast together. What happened was slightly less perfect than I had hoped! It was a freezing cold morning with a frigid wind whipping through the broken windows of the years-unused fire tower. We even had a tired onlooker–a racoon making it’s bed in the ceiling! But, the morning really was crystal clear and the sunrise was beautiful. We read some Psalms together and I told Grace for the first time that I loved her. I asked her to marry me and she quietly said yes. + +And so, the wedding planning began and here we now are, less than two months from the day! What we thought were wonderful days while we were dating paled in comparison to the joy of being engaged, looking forward every moment to the day of our wedding and beyond, all the while getting to know each other in a deeper and closer way. + +Grace and I are so thankful for the goodness of the Lord. That very goodness that He so graciously and patiently taught both of us has now been poured out to us in ways we hardly know how to express. He has given us salvation through His Son, Jesus Christ. He has given us wonderful, strong families who love us and care for us. He has given us so many good, encouraging friendships. He has given us to each other. What a wonderful God we serve! + +Grace and I can think of no better way to return our thanks to the Lord than to serve Him with whole hearts for the rest of our lives. Whatever the Lord has for us, may His glory be shown and His honor upheld through our lives together! That is our desire for our marriage. diff --git a/src/posts/2012/07/25/nested-user-controls-not-rendering.md b/src/posts/2012/07/25/nested-user-controls-not-rendering.md new file mode 100644 index 0000000..aed8843 --- /dev/null +++ b/src/posts/2012/07/25/nested-user-controls-not-rendering.md @@ -0,0 +1,25 @@ +--- +layout: post +title: "Nested User Controls Not Rendering" +date: 2012-07-25 +description: "Nested User Controls Not Rendering" +categories: [programming] +tags: [sitecore] +--- +## Problem +I got caught today in a stupid problem. But, it wasn’t obvious (at least to me) at first what was causing it. I was doing something simple: nesting one UserControl inside of another. But, the nested UserControl wasn’t showing up. I couldn’t, for the life of me, figure out what I had done wrong. If I didn’t nest the control, it worked fine. + +## Solution +The solution was just correctly a stupid oversight on my part. I use ReSharper, so what I would do is just type the control prefix, colon then the name of the control and ReSharper would give me the suggestion to add the right Register tag. Well, ReSharper looks at it like a WebControl, not a UserControl. So, the Register tag is adds looks like this: + +{% highlight csharp %} +<%@ Register TagPrefix="ogden" Namespace="Ogden.Web.controls" Assembly="Ogden.Web" %> +{% endhighlight %} + +It should look like this: + +{% highlight csharp %} +<%@ Register TagPrefix="ogden" TagName="BlogComments" src="BlogComments.ascx" %> +{% endhighlight %} + +So, if you’re dumb like me, make sure you have the right Register tag if you’re nesting a UserControl (or using a UserControl anywhere really). diff --git a/src/posts/2012/07/27/group-list-into-sub-lists-by-index.md b/src/posts/2012/07/27/group-list-into-sub-lists-by-index.md new file mode 100644 index 0000000..bdbaa12 --- /dev/null +++ b/src/posts/2012/07/27/group-list-into-sub-lists-by-index.md @@ -0,0 +1,22 @@ +--- +layout: post +title: "Group List Into Sub-lists by Index" +date: 2012-07-27 +description: "Group List Into Sub-lists by Index" +categories: [programming] +tags: [.net] +--- +## Problem +Everyone once-in-a-while I need to take a flat list (or array, or whatever) of items and divide it up into chunks. It doesn’t have to be in any real order, necessarily, it just has to be in groups of two, three or four or however many items. Most recently, I needed this to divide up a list of blog items into chunks of four to display in sets of “pages” that a user would page through with Javascript. + +## Solution +I found a great, compact solution on Stack Overflow here: [http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq](http://stackoverflow.com/questions/419019/split-list-into-sublists-with-linq). Take a look at answer number one and make sure to pay attention to the warnings others gave about performance for large sets of items. I’m working on a small set, so it’s not too important in my case. + +Here’s the code I used to set a Repeater DataSource: + +{% highlight csharp %} +PodsRepeater.DataSource = Data.Items + .Select((x, i) => new { Index = i, Value = x }) + .GroupBy(obj => obj.Index / 4) + .Select(obj => obj.Select(v => v.Value).ToList()); +{% endhighlight %} diff --git a/src/posts/2012/08/09/rendering-must-have-placeholder-chrome-as-its-parent-got-rendering-instead.md b/src/posts/2012/08/09/rendering-must-have-placeholder-chrome-as-its-parent-got-rendering-instead.md new file mode 100644 index 0000000..21bd35e --- /dev/null +++ b/src/posts/2012/08/09/rendering-must-have-placeholder-chrome-as-its-parent-got-rendering-instead.md @@ -0,0 +1,43 @@ +--- +layout: post +title: "Rendering must have placeholder chrome as its parent. Got ‘rendering’ instead" +date: 2012-08-09 +description: "Rendering must have placeholder chrome as its parent. Got ‘rendering’ instead" +categories: [programming] +tags: [sitecore] +--- +## Problem +I got the error message (in my Firebug console) in the Sitecore Page Editor on a page where I had a nested sublayout. I had a basic two-column layout for the main sublayout of the page. Then, in the right column, I had another sublayout that further divided up the right column as I needed. Here is what the nested sublayout looked like: + +{% highlight html %} + + + + + +{% endhighlight %} + +This was a Javascript error that was preventing the Page Editor from fully loading the editor buttons for the edit frames I had on the page. + +## Solution +The error message (surprise, surprise) was a little mystifying, but it gave me enough of a clue to think that there was some type of nesting issue. I noticed that I had a sc:Placeholder element at the top of my nested sub-layout with nothing wrapping it. Perhaps this was the issue? I tried this edit to my nested sub-layout: + +{% highlight html %} +
+ + + + + +
+{% endhighlight %} + +Notice the single, wrapping div. This worked! So, I’m not sure why, but apparently it solves this nesting problem. Perhaps Sitecore just does not expect a sc:Placeholder to be directly inside of another sc:Placeholder and so wrapping it solves that direct nesting issue. diff --git a/src/posts/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site.md b/src/posts/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site.md new file mode 100644 index 0000000..a373e5b --- /dev/null +++ b/src/posts/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site.md @@ -0,0 +1,13 @@ +--- +layout: post +title: "'Specified method is not supported' error when deleting SharePoint site" +date: 2012-09-25 +description: "'Specified method is not supported' error when deleting SharePoint site" +categories: [programming] +tags: [sharepoint] +--- +I was getting a “Specified method is not supported” error when I tried to delete some test sites in SharePoint 2010 today. I found a great link that gave several solutions to deletion errors. Issue #1 under this link is what I needed: + +[SharePoint 2010: Unable to delete site/web after SP1](http://www.benramey.com/2012/09/25/specified-method-is-not-supported-error-when-deleting-sharepoint-site/#) + +Even though I got an error executing the Upgrade-SPContentDatabase PowerShell command, it still fixed the problem with deleting the sites. diff --git a/src/posts/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010.md b/src/posts/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010.md new file mode 100644 index 0000000..6a14f1f --- /dev/null +++ b/src/posts/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "Deploying SharePoint 2010 Page Layouts with Visual Studio 2010" +date: 2012-09-26 +description: "Deploying SharePoint 2010 Page Layouts with Visual Studio 2010" +categories: [programming] +tags: [sharepoint] +--- +[Deploying SharePoint 2010 Page Layouts with Visual Studio 2010](http://www.benramey.com/2012/09/26/deploying-sharepoint-2010-page-layouts-with-visual-studio-2010/#) diff --git a/src/posts/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls.md b/src/posts/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls.md new file mode 100644 index 0000000..eabc501 --- /dev/null +++ b/src/posts/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls" +date: 2012-09-26 +description: "SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls" +categories: [programming] +tags: [sharepoint] +--- +[SharePoint Page Layout Error: Only Content controls are allowed directly in a content page that contains Content controls](http://www.benramey.com/2012/09/26/sharepoint-page-layout-error-only-content-controls-are-allowed-directly-in-a-content-page-that-contains-content-controls/#) diff --git a/src/posts/2012/09/27/cannot-remove-file-error-code-158.md b/src/posts/2012/09/27/cannot-remove-file-error-code-158.md new file mode 100644 index 0000000..1d7ea72 --- /dev/null +++ b/src/posts/2012/09/27/cannot-remove-file-error-code-158.md @@ -0,0 +1,78 @@ +--- +layout: post +title: "Cannot remove file “master_page_name_here”. Error Code: 158" +date: 2012-09-27 +description: "Cannot remove file “master_page_name_here”. Error Code: 158" +categories: [programming] +tags: [sharepoint] +--- + UPDATE (9/19/2013): + Please see Jeff’s comment below for a better solution to this issue using best-practices for iterating through a SPSite’s AllWebs collection. + There are multiple places online that provide an answer to the error in the title of this post. The scenario is pretty simple: deleting a master page file previously deployed by a feature in the FeatureDeactivating method of a FeatureReceiver. + +Posts like this one [http://sharepoint.stackexchange.com/questions/41358/delete-custom-master-page-error-on-feature-deactivation](http://sharepoint.stackexchange.com/questions/41358/delete-custom-master-page-error-on-feature-deactivation) give the right answer (even if it’s sloppy–you don’t need to loop through EACH SPWeb and try to delete the files) but you can still get the error if you’re not careful (like I wasn’t!). + +Here was my specific case. In my FeatureDeactivating method I was setting all the SPWebs back to the original v4.master like this: + +{% highlight csharp %} +foreach (SPWeb site in siteColl.AllWebs) +{ + site.MasterUrl = masterUrl; + site.CustomMasterUrl = masterUrl; + site.Update(); +} +{% endhighlight %} + +Notice how I am looping through the SPSite.AllWebs SPWebCollection and updating each SPWeb after I reset the master page URLs. Now, here is what I was doing to try to delete the master page file from the _catalogs/masterpage library: + +{% highlight csharp %} +string fileUrl = SPUrlUtility.CombineUrl( + siteColl.ServerRelativeUrl, + file.FullRelativeUrl); +SPFile spFile = siteColl.RootWeb.GetFile(fileUrl); +try +{ + if (spFile.Exists) + { + spFile.Delete(); + spFile.Update(); + } +} +catch { } +{% endhighlight %} + +When I would get to the spFile.Delete() line, it would catch the exception whose error message is the title of this post. But why?? The master page wasn’t being referenced anywhere anymore! As I would discover, the problem is on this line: + +{% highlight csharp %} +SPFile spFile = siteColl.RootWeb.GetFile(fileUrl); +{% endhighlight %} + +Do you see it? I didn’t at first either. The RootWeb SPWeb reference isn’t pointing to the same object that the same SPWeb in the SPSite.AllWebs collection is pointing to. So, technically, the RootWeb object still thinks its master page URLs are pointing to my custom master that I’m trying to delete because it hasn’t had Update() called on it. + +To fix it, I had to get my SPFile from one of the SPWebs in SPSite.AllWebs, like this: + +{% highlight csharp %} +SPFile spFile = siteColl.AllWebs.First().GetFile(fileUrl); +{% endhighlight %} + +That enables me to delete the file with no errors. + +## Jeff's comment +Hi Ben, +Great post but I did run into the same issue you were experiencing today. You can fix this issue also by using recommended best practice ([http://msdn.microsoft.com/en-us/library/aa973248(v=office.12).aspx](http://msdn.microsoft.com/en-us/library/aa973248(v=office.12).aspx)) – + +Good Coding Practice #2 + +When iterating through SPWebs dispose of items with the using statement. This will cleanup any issues left in memory + +So it might look like this: + +{% highlight csharp %} +using(SPSite siteColl = properties.Feature.Parent as SPSite) +{ + foreach (SPWeb site in siteColl.AllWebs) + { + site.MasterUrl = masterUrl; site.CustomMasterUrl = masterUrl; site.Update(); + } +} +{% endhighlight %} diff --git a/src/posts/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection.md b/src/posts/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection.md new file mode 100644 index 0000000..1ed5018 --- /dev/null +++ b/src/posts/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "Endless Authentication Prompts for SharePoint Site Collection" +date: 2012-09-27 +description: "Endless Authentication Prompts for SharePoint Site Collection" +categories: [programming] +tags: [sharepoint] +--- +[Fix for endless authentication prompts in SharePoint on localhost with host header](http://www.benramey.com/2012/09/27/endless-authentication-prompts-for-sharepoint-site-collection/#) diff --git a/src/posts/2012/09/28/check-for-a-publishing-spweb-or-spsite.md b/src/posts/2012/09/28/check-for-a-publishing-spweb-or-spsite.md new file mode 100644 index 0000000..8532497 --- /dev/null +++ b/src/posts/2012/09/28/check-for-a-publishing-spweb-or-spsite.md @@ -0,0 +1,9 @@ +--- +layout: post +title: "Check for a Publishing SPWeb or SPSite" +date: 2012-09-28 +description: "Check for a Publishing SPWeb or SPSite" +categories: [programming] +tags: [sharepoint] +--- +[Check for a Publishing SPWeb or SPSite](http://www.dhirendrayadav.com/2011/09/checking-if-site-is-publishing-site.html) diff --git a/src/posts/2012/10/05/retrieve-dynamic-module-choice-field-value-options-in-sitefinity.md b/src/posts/2012/10/05/retrieve-dynamic-module-choice-field-value-options-in-sitefinity.md new file mode 100644 index 0000000..dfdf21b --- /dev/null +++ b/src/posts/2012/10/05/retrieve-dynamic-module-choice-field-value-options-in-sitefinity.md @@ -0,0 +1,104 @@ +--- +layout: post +title: "Retrieve Dynamic Module Choice Field Value Options in Sitefinity" +date: 2012-10-05 +description: "Retrieve Dynamic Module Choice Field Value Options in Sitefinity" +categories: [programming] +tags: [sitefinity] +--- +I am testing out Sitefinity right now for an upcoming project. So far, I really like everything I see. It’s a great CMS and should fit out needs just perfectly (for pretty cheap too). + +As with all software though, you will inevitably run into things you can’t do so easily (or, at least, as easily as you think you should be able to). I ran into one of those issues yesterday and finally hacked my way to a solution this morning. + +## Problem +My problem was this: how do I retrieve the available options for a Choices field in a dynamic module (built with the Module Builder). Honestly, I’m not sure it really matters. That is, I don’t think Sitefinity validates the value of a Choices field against what is technically a “valid” option. I think I read somewhere that it can store any option. However, I wanted to present a drop-down to users that had values that were essentially controlled by the available choices for this field. Here’s the dialog I’m talking about: + +![Choices field choice options](/assets/images/2012-10-05-retrieve-dynamic-module-choice-field-value-options-in-sitefinity/choices_dialog.png) + +## Solution +Finding a way to retrieve this options through the Sitefinity API (which, in all other respects I’ve investigated so far is really good) was impossible. I started decompiling Sitefinity DLLs to see how it was done and still didn’t really get anywhere. All I could see was the choices being picked up from a config file. + +As I read more, I realized this was a bigger clue than I initially thought. The dynamic module information is stored in the App_Data/Sitefinity/Configuration directory in the DynamicModulesConfig.config file. Lo and behold, the options were stored in there–in no less than three places too. Sitefinity has the Config.Get interface for it’s configuration files. However, to get the DynamicModulesConfig.config information, I would have had to call something like Config.Get(). But, the DynamicModulesConfig (in Telerik.Sitefinity.DynamicModules.Configuration) is an internal class. So, I couldn’t do that. I decided upon a “get config file and parse the xml” approach. + +Here is my eventual solution. Unless they change the file location or configuration XML structure, it’s pretty safe, albeit a little hacky. I created a simple DynamicModulesHelper class. + +{% highlight csharp %} +public static class DynamicModuleHelper +{ + public const string DynamicModulesConfigRelativePath = + "~/App_Data/Sitefinity/Configuration/DynamicModulesConfig.config"; + + public static KeyValuePair[] GetChoiceFieldOptions( + string contentType, + string dataFieldName) + { + XmlNodeList choicesNodes = GetChoiceNodes(contentType, dataFieldName); + if (choicesNodes == null) + { + return new KeyValuePair[0]; + } + + var choices = new List>(); + foreach (XmlNode node in choicesNodes) + { + ParseChoiceElement(choices, node); + } + + return choices.ToArray(); + } + + private static void ParseChoiceElement( + List> choices, + XmlNode node) + { + if (node == null || node.Attributes == null) + { + return; + } + + string key, value; + key = value = string.Empty; + if (node.Attributes["text"] != null) + { + key = node.Attributes["text"].Value; + } + if (node.Attributes["value"] != null) + { + value = node.Attributes["value"].Value; + } + + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) + { + choices.Add(new KeyValuePair(key, value)); + } + } + + private static XmlNodeList GetChoiceNodes( + string contentType, + string dataFieldName) + { + string dynamicModulesConfigPath = HttpContext.Current.Server.MapPath( + DynamicModulesConfigRelativePath); + XmlDocument xml = new ConfigXmlDocument(); + string xpath = string.Concat( + "/dynamicModulesConfig/contentViewControls", + string.Format("/contentViewControl[@contentType='{0}’]", contentType), + "//view[@displayMode=’Read’]", + string.Format("//field[@dataFieldName='{0}’]", dataFieldName), + "/choicesConfig/element" + ); + + xml.Load(dynamicModulesConfigPath); + XmlNodeList choicesNodes = xml.SelectNodes(xpath); + return choicesNodes; + } +} +{% endhighlight %} + +To get the Choices field options, you call the GetChoiceFieldOptions method, passing in the dynamic module’s content type string (like this one: “Telerik.Sitefinity.DynamicTypes.Model.TestModule.TestModule”) and the name of the Choices field (like this: “RandomChoices”). So, my particular call for just a goofy test module I created was this: + +{% highlight csharp %} +KeyValuePair[] choices = DynamicModuleHelper.GetChoiceFieldOptions( + "Telerik.Sitefinity.DynamicTypes.Model.TestModule.TestModule", + "RandomChoices"); +{% endhighlight %} diff --git a/src/posts/2012/10/10/sitefinity-configuration-file-changes-when-creating-and-activating-a-dynamic-module.md b/src/posts/2012/10/10/sitefinity-configuration-file-changes-when-creating-and-activating-a-dynamic-module.md new file mode 100644 index 0000000..5c54d6f --- /dev/null +++ b/src/posts/2012/10/10/sitefinity-configuration-file-changes-when-creating-and-activating-a-dynamic-module.md @@ -0,0 +1,239 @@ +--- +layout: post +title: "Sitefinity Configuration File Changes When Creating and Activating a Dynamic Module" +date: 2012-10-10 +description: "Sitefinity Configuration File Changes When Creating and Activating a Dynamic Module" +categories: [programming] +tags: [sitefinity] +--- +After you create and activate a dynamic module with Module Builder in Sitefinity several configuration files are changes inside of App_Data/Sitefinity/Configuration. Here’s a list of what is changed. + +## ContentViewConfig.config +A config:link element is added under contentViewControls. Example: +{% highlight xml %} + +{% endhighlight %} + +## DynamicModulesConfig.config +And entire contentViewControl section is added under the contentViewControls element. Example: + +{% highlight xml %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
+ + +
+ + + + +
+
+ + + + +
+
+
+ + + + +## BaseDataContext + + +## EFDataContext + + +## IRepository + + +## EFRepository +Not every method is implemented because I didn't need them all on this project. Using TDD, I only implemented methods as I needed them. + + +## EFDbContext +This is the project-specific EntityFramework DbContext. + + +## EFSagaPersister + + +## EFSubscriptionPersister + + +## EFTimeoutPersister + + +## TimeoutDataEntity + + +## Subscription + + +## SagaData + + +## Criteria classes + diff --git a/src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md b/src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md new file mode 100644 index 0000000..f0ffab0 --- /dev/null +++ b/src/posts/2014/03/21/the-dumbest-code-i-ever-wrote.md @@ -0,0 +1,14 @@ +--- +layout: post +title: "The Dumbest Code I Ever Wrote" +date: 2014-03-21 +description: "The Dumbest Code I Ever Wrote" +categories: [programming] +tags: [] +--- + +Here it is, folks: some of the dumbest code I’ve ever written and I just now noticed it. + +{% highlight csharp %} +string.Concat("attachment; filename=\"", string.Concat(pagePdf.Name, ".pdf"), "\""); +{% endhighlight %} diff --git a/src/posts/2014/03/27/episerver-7-the-file-linkguid-aspx-does-not-exist-error.md b/src/posts/2014/03/27/episerver-7-the-file-linkguid-aspx-does-not-exist-error.md new file mode 100644 index 0000000..35f63f5 --- /dev/null +++ b/src/posts/2014/03/27/episerver-7-the-file-linkguid-aspx-does-not-exist-error.md @@ -0,0 +1,32 @@ +--- +layout: post +title: "EPiServer 7 “The file ‘/link/GUID.aspx’ does not exist.” Error" +date: 2014-03-27 +description: "EPiServer 7 “The file ‘/link/GUID.aspx’ does not exist.” Error" +categories: [programming] +tags: [episerver] +--- + +I ran into the following error while working with an installation of EPiServer 7 this week. It took me a long time to finally figure out the solution. + +First, here’s the error and stack trace: + +{% highlight text %} +2014-03-27 12:12:43,396 [6] ERROR EPiServer.Global: 1.2.5 Unhandled exception in ASP.NET +System.Web.HttpException (0x80004005): The file ‘/link/cc299342a697494c8a4bc47717210bf0.aspx’ does not exist. +at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate) +at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate) +at System.Web.Compilation.BuildManager.GetVirtualPathObjectFactory(VirtualPath virtualPath, HttpContext context, Boolean allowCrossApp, Boolean throwIfNotFound) +at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp) +at System.Web.Routing.PageRouteHandler.GetHttpHandler(RequestContext requestContext) +at System.Web.Routing.UrlRoutingModule.PostResolveRequestCache(HttpContextBase context) +at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +{% endhighlight %} + +As it turns out, I did not have a TemplateDescriptor attribute on the page template that I was trying to access. Adding the following TemplateDescriptor attribute to the code-behind class for my page template saved me. + +{% highlight csharp %} +[TemplateDescriptor(Path = "~/Templates/PageTemplates/Home.aspx")] +public partial class Home : TemplatePage +{% endhighlight %} diff --git a/src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md b/src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md new file mode 100644 index 0000000..7c86478 --- /dev/null +++ b/src/posts/2014/04/17/episerver-7-cannot-decrypt-password.md @@ -0,0 +1,47 @@ +--- +layout: post +title: "EPiServer 7: Cannot decrypt password" +date: 2014-04-17 +description: "EPiServer 7: Cannot decrypt password" +categories: [programming] +tags: [episerver] +--- + +## Problem +I’ve been dealing with an issue in EPiServer 7 recently. I got this error when trying to log into the admin back-end of the site. + +{% highlight text %} +Cannot decrypt password + +Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. + +Exception Details: System.InvalidOperationException: Cannot decrypt password + +Source Error: + +An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below. + +Stack Trace: + +[InvalidOperationException: Cannot decrypt password] +EPiServer.Common.Security.HMACPasswordProvider.DecryptPassword(Byte[] ciphertext) +60 +EPiServer.Common.Web.Authorization.Integrator.SynchronizeUser(MembershipUser membershipUser, String password, Boolean enableCreateNew) +1116 +System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80 +System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +165 +{% endhighlight %} + +## Scenario +We originally installed the Relate+ site for our EPiServer installation. Through various bad decisions, we didn’t keep the custom code and templates we wrote separate from the Relate+ stuff. So, we inevitably created a mess where we did not need a ton of the Relate+ templates, page types, block types, etc but we could not longer easily pull our custom stuff out from the tangled mess. + +Well, we undertook the disentangling anyway (a good decision) and ended up with a much cleaner code base. This meant re-installing EPiServer for a clean (without Relate+) beginning. The database would remain the same to keep our content. + +We ran into this error when we got the new, cleaned-up site installed and running and then tried to log into the administration portion of the site. + +## Solution +At first, I found this post online: http://world.episerver.com/Modules/Forum/Pages/Thread.aspx?id=77776 which suggested changing over to the multiplexing membership and role providers and described the new hashing mechanism in EPiServer 7. + +At first, this seemed to work. Yay! But, the joy didn’t last long. I soon got the error again even after using the multiplexing providers. I dug a little deeper into the issue. + +What I found was (in retrospect) pretty obvious. When we did the new, clean installation of EPiServer, it also created a new web.config with new attributes on the machineKey element. These properties are used with forms authentication to encrypt and decrypt information. Well, we didn’t make sure to keep the same values between the old install and the new install. So, when it tried to decrypt login information created with the old descryptionKey using the new decryptionKey, it obviously didn’t work. + +Luckily, our trusty Systems Admins always keep backups of stuff they replace. We just went into the old web.config file from the previous site, copied the entire element and overwrite it in the new web.config. After an app pool recycle, the login was working great. diff --git a/src/posts/2014/05/21/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2.md b/src/posts/2014/05/21/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2.md new file mode 100644 index 0000000..f439fed --- /dev/null +++ b/src/posts/2014/05/21/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2.md @@ -0,0 +1,133 @@ +--- +layout: post +title: "Create a List Selector Field in a Widget Designer in Sitefinity 6.2" +date: 2014-05-21 +description: "Create a List Selector Field in a Widget Designer in Sitefinity 6.2" +categories: [programming] +tags: [sitefinity] +--- + +> [9/25/14 update] See Hardy's comments below for necessary updates to my steps when using Sitefinity 7.1 and up. Thanks, Hardy! + +## Scenario + +I am designing a fairly simple widget in Sitefinity (version 6.2, but this probably applies to other versions as well) that will display a simple FAQ list. The FAQ list has two major parts: 1) a "table of contents" type ordered-list at the top that lists each question and links to the answer further down the page, and 2) the list of questions with the answers below them. + +## Approach + +I wanted to create a simple Sitefinity list that would contain the FAQ content (questions and answers). The default fields for a Sitefinity list are Title and Content which are really all I needed to collect a Question and Answer for a FAQ item. + +So, my widget, after being placed on a page, would only need to allow a content author to simply select the FAQ list they had created earlier. The widget would then find the list, collect the items in the list and display them. + +## Solution + +My solution would involved a widget designer control and a customized FlatSelector field. Here's an overview with details below. + +### Overview +1. Create a Sitefinity MVC widget with a single "List" field that will contain the list GUID +1. Create a Sitefinity widget designer for this MVC widget with a single FlatSelector field + 1. Update the FlatSelector ItemType field + 1. Update the FlatSelector ServiceURL field + 1. Update the FlatSelector DataMembers + 1. Update the widget designer Javascript to choose the list ID + + +### Details +For some reason, Sitefinity widget designer documentation is really hard for me to find. Maybe I'm the only one. Maybe Sitefinity needs to put some more dollars toward good documentation! Anyway, after some searching and some trial and error, I found this page giving me an idea of how I should accomplish what I was trying to do: [Sitefinity article][sf-article] + +To summarize, it tells you how to create a widget designer with Sitefinity Thunder and then change the generated control to point to some of the generic content types (like Lists). However, it didn't have all the details I needed. I had to troubleshoot and that's why I'm writing this post. + +#### Step 1: Create the Widget +Below are the details for how I setup my MVC widget. Translate the various tasks for the same effect if you prefer a WebForms widget. + +Using Sitefinity Thunder, create your widget. Don't create the designer at this time. I like using MVC widgets but there's no reason this won't work with a WebForms widget. I called my widget "FAQList". + +##### Step 1.1: Update controller properties +Remove whatever properties Thunder automatically adds on the MVC controller and add your own List property of type Guid. + +{% highlight csharp %} +[Category("Widget Properties")] +public Guid List { get; set; } +{% endhighlight %} + +##### Step 1.2: Update widget view model +I added a simple POCO to describe each FAQ item (containing a question and answer) and then added a list of these POCOs as the only property on my view model. + +{% highlight csharp %} +public class FAQListModel +{ + public List Items { get; set; } +} + +public class FAQItem +{ + public string Answer { get; set; } + public string Question { get; set; } +} +{% endhighlight %} + +##### Step 1.3: Update Index action +Update the Index action to use the List property to access the content-author-chosen list, pull the items and stick them in the model. + +{% highlight csharp %} +public ActionResult Index() +{ + var model = new FAQListModel(); + + if (List != Guid.Empty) + { + model.Items = App.WorkWith() + .List(List) + .ListItems() + .Get() + .Where(li => li.Status == ContentLifecycleStatus.Live) + .Select(li => new FAQItem { Question = li.Title, Answer = li.Content }) + .ToList(); + } + else + { + model.Items = new List(); + } + + return View("Default", model); +} +{% endhighlight %} + +#### Step 2: Create widget designer/public +Once your widget is created, you can create your widget designer. Using Thunder, create a widget designer for an existing widget. Name your widget designer. I use the name of the widget and append "Designer". + +![FAQItemDesigner widget designer naming](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-naming.png) + +Click "Next" on the next screen. Choose your FAQItemController from the list on the next screen. + +##### Step 2.1: Choose widget designer fields +Next, choose the List property from your controller and set it up as follows. Especially note two things. First, choose the DynamicContentSelector. Second, update the "Select the content type for the selector" to 'Telerik.Sitefinity.Lists.Model.List'. Click on "Add". + +![Create a widget designer](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-fields1.png) + +##### Step 2.2: Update generated widget designer files +In the generated .ascx file, update the 'ServiceUrl' property on the FlatSelector control to '/Sitefinity/Services/Lists/ListService.svc/?managerType=&providerName=&itemType=Telerik.Sitefinity.Lists.Model.List&provider=&sortExpression=LastModified%20DESC&skip=0&take=50'. + +![Service URL 1](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-serviceurl1.png) + +In the generated .cs file, remove line 135 that sets the ConstantFilter property to 'Visible=true'. + +![FAQItemDesigner remove line 135](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-removeline135.png) + +Finally, in the generated .js file, change the property that is used as the value of the selected list from 'OriginalContentId' to 'Id'. This code can be found in the '_ListDoneSelecting' method. + +![FAQItemDesigner update js file](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-changejsfile.png) + +At this point, you should be done! Build your project, plop your new widget on a page and test it out! Here's what mine looks like. + +The edit dialog: +![FAQItemDesigner edit screen](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-editscreen.png) + +The list select screen: +![FAQItemDesigner select screen](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-selectscreen.png) + +The selected item screen: +![FAQItemDesigner selected screen](/assets/images/create-a-list-selector-field-in-a-widget-designer-in-sitefinity-6-2/widget-designer-selectedscreen.png) + + +[sf-article]: http://www.sitefinity.com/documentation/documentationarticles/change-your-dynamic-content-selector-to-choose-from-generic-content diff --git a/src/posts/2014/05/23/sitefinity-thunder-boolean-widget-designer-field-auto-generation-is-flawed.md b/src/posts/2014/05/23/sitefinity-thunder-boolean-widget-designer-field-auto-generation-is-flawed.md new file mode 100644 index 0000000..e545ea9 --- /dev/null +++ b/src/posts/2014/05/23/sitefinity-thunder-boolean-widget-designer-field-auto-generation-is-flawed.md @@ -0,0 +1,32 @@ +--- +layout: post +title: "Sitefinity Thunder Boolean Widget Designer Field Auto-Generation is Flawed" +date: 2014-05-23 +description: "Sitefinity Thunder Boolean Widget Designer Field Auto-Generation is Flawed" +categories: [programming] +tags: [sitefinity] +--- + +## Problem + +When you use Sitefinity Thunder (a great help, by the way) to generate a Widget Designer for an existing widget that has a boolean property, you get this javascript in the auto-generated javascript file (let’s say my boolean field is named HasSubMenu): + +{% highlight javascript %} +/* RefreshUI HasSubMenu */ +jQuery(this.get_hasSubMenu()).attr("checked", controlData.HasSubMenu); +{% endhighlight %} + +This code is in the refreshUI method. It’s supposed to mark a checkbox as checked if HasSubMenu is true and not check it if it’s false. The problem is, for some reason, HasSubMenu is a string in Javascript instead of a real boolean. So, HasSubMenu will be either “true” or “false”–both of which are true values in Javascript. That is, because “true” and “false” are both non-empty strings, they evaluate to true when used as an expression. + +This means, no matter what you do with the checkbox, the next time you click “Edit” for this widget, the checkbox will be checked and your data is effectively corrupted. + +## Solution + +The solution is pretty simple. Instead of using controlData.HasSubMenu for the expression by itself, update your Javascript so that you check if the string is “true” or not: controlData.HasSubMenu === "true". The line in the refreshUI method should now look like this: + +{% highlight javascript %} +/* RefreshUI HasSubMenu */ +jQuery(this.get_hasSubMenu()).attr("checked", controlData.HasSubMenu === "true"); +{% endhighlight %} + +Now your checkbox will keep it’s value as you would expect. diff --git a/src/posts/2014/06/05/ie-date-parsing-doesnt-work-like-chrome.md b/src/posts/2014/06/05/ie-date-parsing-doesnt-work-like-chrome.md new file mode 100644 index 0000000..4a4fe27 --- /dev/null +++ b/src/posts/2014/06/05/ie-date-parsing-doesnt-work-like-chrome.md @@ -0,0 +1,30 @@ +--- +layout: post +title: "IE Date Parsing Doesn’t Work Like Chrome" +date: 2014-06-05 +description: "IE Date Parsing Doesn’t Work Like Chrome" +categories: [programming] +tags: [ie,chrome] +--- + +I noticed a goofy issue today while working with a jQuery countdown plugin. This plugin allows you to set an “until” date so that you counter counts down to zero at a certain date and time. + +I was setting the date like this: + +{% highlight javascript %} +element.countdown({ +until: new Date(’10/13/14 00:00:00′) +}); +{% endhighlight %} + +In Chrome, this worked just fine. The string “10/13/14 00:00:00″ parsed to a Date object for midnight on October 13th, 2014. + +In IE, however (versions10, 9 and 8 at least) it parsed as October 13th, 1914. Why? Who knows. Updating my string to “10/13/2014 00:00:00″ fixed it. My final javascript looked like this: + +{% highlight javascript %} +element.countdown({ +until: new Date(’10/13/2014 00:00:00′) +}); +{% endhighlight %} + +So, be aware of this oddity! It caused my whole countdown widget to screw up because the current time was already past the “until” date.# diff --git a/src/posts/2014/09/23/apply-git-patches-to-svn-repository.md b/src/posts/2014/09/23/apply-git-patches-to-svn-repository.md new file mode 100644 index 0000000..f181cd2 --- /dev/null +++ b/src/posts/2014/09/23/apply-git-patches-to-svn-repository.md @@ -0,0 +1,10 @@ +--- +layout: post +title: "Apply Git Patches to SVN Repository" +date: 2014-09-23 +description: "Apply Git Patches to SVN Repository" +categories: [programming] +tags: [git] +--- + +[Creating Subversion patches with Git](http://codeprairie.net/blogs/chrisortman/archive/2008/01/14/creating-subversion-patches-with-git.aspx) diff --git a/src/posts/2014/10/20/active-directory-authentication-in-asp-net-mvc-5-with-forms-authentication-and-group-based-authorization.md b/src/posts/2014/10/20/active-directory-authentication-in-asp-net-mvc-5-with-forms-authentication-and-group-based-authorization.md new file mode 100644 index 0000000..fd6ac82 --- /dev/null +++ b/src/posts/2014/10/20/active-directory-authentication-in-asp-net-mvc-5-with-forms-authentication-and-group-based-authorization.md @@ -0,0 +1,188 @@ +--- +layout: post +title: "Active Directory Authentication in ASP.NET MVC 5 with Forms Authentication and Group-Based Authorization" +date: 2014-10-20 9:03:00 +description: "How to integrate MVC authorization attributes with Active Directory." +categories: [programming] +tags: [mvc,asp.net,active-directory] +--- + +I know that blog post title is sure a mouth-full, but it describes the whole problem I was trying to solve in a recent project. + +## The Project + +Let me outline the project briefly. We were building a report dashboard-type site that will live inside the client's network. The dashboard gives an overview of various, very important information that relates to how the company is performing on a hourly basis. So, the dashboard is only available to a certain group of directors. + +To limit the solution to the these directors, authentication and authorization would go through their existing Active Directory setup by putting the authorized users in a special AD group. + +## The Problem + +Getting authentication to work was a snap. Microsoft provides the System.Web.Security.ActiveDirectoryMembershipProvider +class to use as your membership provider. Putting an `[Authorize]` attribute on my action methods or entire controllers was all I needed to get it working (besides, of course, the system.web/authentication web.config updates and a controller to show my login form and handle the submit credentials). + +Here's my relevant web.config setup: + +{% highlight xml %} + + + +… + + + + + + + + + +{% endhighlight %} + +The tough part came when I wanted to limit access to users in that AD group. Microsoft doesn't provide a RoleProvider along with its ActiveDirectoryMembershipProvider. So, what to do? + +I tried several methods I found online. Most of them were based on creating my own custom RoleProvider and querying AD to iterate through the user's groups (treating them like roles) and seeing if one of them matched my AD group I was looking for. However, I could never get it to work. Each code example I found eventually gave me this AD error when I iterated through the current user's AD groups: + +{% highlight text %} +The specified directory service attribute or value does not exist. +{% endhighlight %} + +## The Solution + +Eventually, I found a solution online that worked. Instead of setting up a custom RoleProvider, all it involved was creating a custom AuthorizeAttribute for your MVC controllers (or action methods) that checked the user's .IsMemberOf method to see if the member belonged the sought after group (or groups). I don't know why this method does not cause the same AD error as describe above, but I'm glad it doesn't! All I can assume is that it queries AD in a more friendly way. + +Here is my custom AuthorizeAttribute: + +{% highlight c# %} +public class AuthorizeADAttribute : AuthorizeAttribute +{ + private bool _authenticated; + private bool _authorized; + + public string Groups { get; set; } + + protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) + { + base.HandleUnauthorizedRequest(filterContext); + + if (_authenticated && !_authorized) + { + filterContext.Result = new RedirectResult("/error/notauthorized"); + } + } + + protected override bool AuthorizeCore(HttpContextBase httpContext) + { + _authenticated = base.AuthorizeCore(httpContext); + + if (_authenticated) + { + if (string.IsNullOrEmpty(Groups)) + { + _authorized = true; + return _authorized; + } + + var groups = Groups.Split(','); + string username = httpContext.User.Identity.Name; + + try + { + _authorized = LDAPHelper.UserIsMemberOfGroups(username, groups); + return _authorized; + } + catch (Exception ex) + { + this.Log().Error(() => "Error attempting to authorize user", ex); + _authorized = false; + return _authorized; + } + } + + _authorized = false; + return _authorized; + } +} +{% endhighlight %} + +Notice that I also included a little code to distinguish between the user not being authenticated (which the call to base.AuthorizeCore takes care of) and not being authorized. Without the code in HandleUnauthorizedRequest, if the user successfully logs in but is not in the AD group, he just sees the log in screen again which doesn't communicate the problem very well. + +The this.Log() code uses a Nuget package called this.Log. The LDAPHelper class is something I wrote. The code is below: + +{% highlight c# %} +public static class LDAPHelper +{ + public static string GetLDAPContainer() + { + Uri ldapUri; + ParseLDAPConnectionString(out ldapUri); + + return HttpUtility.UrlDecode(ldapUri.PathAndQuery.TrimStart('/')); + } + + public static string GetLDAPHost() + { + Uri ldapUri; + ParseLDAPConnectionString(out ldapUri); + + return ldapUri.Host; + } + + public static bool ParseLDAPConnectionString(out Uri ldapUri) + { + string connString = ConfigurationManager.ConnectionStrings["ADConnectionString"].ConnectionString; + + return Uri.TryCreate(connString, UriKind.Absolute, out ldapUri); + } + + public static bool UserIsMemberOfGroups(string username, string[] groups) + { + /* Return true immediately if the authorization is not + locked down to any particular AD group */ + if (groups == null || groups.Length == 0) + { + return true; + } + + // Verify that the user is in the given AD group (if any) + using (var context = BuildPrincipalContext()) + { + var userPrincipal = UserPrincipal.FindByIdentity(context, + IdentityType.SamAccountName, + username); + + foreach (var group in groups) + { + if (userPrincipal.IsMemberOf(context, IdentityType.Name, group)) + { + return true; + } + } + } + + return false; + } + + public static PrincipalContext BuildPrincipalContext() + { + string container = LDAPHelper.GetLDAPContainer(); + return new PrincipalContext(ContextType.Domain, null, container); + } +} +{% endhighlight %} + +My code is mostly based on example code I found on a very helpful [StackOverflow post][so-post]. + +To use this code, all you have to do is use your custom AuthorizeAttribute instead of the built-in one. Something like this: + +{% highlight c# %} +[AuthorizeAD(Groups="Some AD group name")] +public class HomeController : Controller +{ +… +} +{% endhighlight %} + +[so-post]: http://stackoverflow.com/questions/4342271/asp-net-mvc-forms-authorization-with-active-directory-groups/4383502#4383502 diff --git a/src/posts/2015/09/03/permutations-with-iterators-in-csharp.md b/src/posts/2015/09/03/permutations-with-iterators-in-csharp.md new file mode 100644 index 0000000..049e185 --- /dev/null +++ b/src/posts/2015/09/03/permutations-with-iterators-in-csharp.md @@ -0,0 +1,25 @@ +--- +layout: post +title: "Permutations with Iterators in C#" +date: 2015-09-03 12:27:00 +description: "Link to great code for generator permutations of objects with iterators" +categories: [programming] +tags: [.net] +--- + +I recently ran across a need to generate all permutations of an array of objects +in some unit testing I was doing. + +The specific situation was testing that, no matter what the order, when certain messages +were picked up by an [NServiceBus][nsbus] [Saga][sagas], that a certain state was consistent after all +messages were received. Since I explicitly wanted to test the state no matter what order +the messages arrived in, I needed all permutations of those messages so I could send +them to the Saga in each order and test the resulting state. + +I found this super article on the subject that gave all the code I needed: [Generating Permutations with C# Iterators][article]. + +Thanks IanG on Tap! + +[nsbus]:http://particular.net/ +[article]:http://www.interact-sw.co.uk/iangblog/2004/09/16/permuterate +[sagas]:particular.net/articles/sagas-in-nservicebus diff --git a/src/posts/2015/12/02/working-with-x509-certificates-in-net.md b/src/posts/2015/12/02/working-with-x509-certificates-in-net.md new file mode 100644 index 0000000..060ad82 --- /dev/null +++ b/src/posts/2015/12/02/working-with-x509-certificates-in-net.md @@ -0,0 +1,16 @@ +--- +layout: post +title: "Working with x.509 Certificates in .NET" +date: 2015-12-02 16:35:00 +description: "Link to helpful article on working with certificates in Windows and .NET" +categories: [programming] +tags: [.net,certificates] +--- + +I found a lot of very helpful details about certificate storage and usage in Windows and +.NET in this article. + +Particularly, the explanation of where certificates and private keys can be stored for UserStore +certificates was very helpful in debugging some issues I'm currently having on a production server. + +[Paul Stovell's blog post](http://paulstovell.com/blog/x509certificate2) diff --git a/src/posts/2016/01/04/adding-a-rabbitmq-configuration-file-when-running-as-a-windows-service.md b/src/posts/2016/01/04/adding-a-rabbitmq-configuration-file-when-running-as-a-windows-service.md new file mode 100644 index 0000000..f79e4eb --- /dev/null +++ b/src/posts/2016/01/04/adding-a-rabbitmq-configuration-file-when-running-as-a-windows-service.md @@ -0,0 +1,43 @@ +--- +layout: post +title: "Adding a RabbitMQ Configuration File When Running as a Windows Service" +date: 2016-01-04 15:26:00 +description: "Instructions to add a RabbitMQ configuration file after having installed the Windows Service already." +categories: [programming] +tags: [rabbitmq] +--- +## A little context +I was having a hard time adding a rabbitmq.config file today. I added it in the right location, restarted +the RabbitMQ Windows service and the logs still showed that the configuration file was not found. + +If you're experiencing the same problem, you'll see a similar error message in the RabbitMQ log file after +you restart the Windows service. + +{% highlight text %} +=INFO REPORT==== +node : rabbit@ +home dir : C:\Windows +config file(s) : c:/path/to/config/rabbitmq.config (not found) +...some other stuff... +{% endhighlight %} + +This will happen if you just install your RabbitMQ server with the normal, default installation process +and then try to add a configuration file later. By default, RabbitMQ doesn't install configuration file and just +uses all it's defined defaults. + +## Solution +I read the [documentation](http://www.rabbitmq.com/configure.html) a little more closely +and finally came upon this line: "Windows service users will need to re-install the service after adding +or removing a configuration file." + +Oops. Guess I should have read the documentation more closely the first time around! The easiest way to +do this is as follows (start a command prompt as administrator): + +{% highlight text %} +> cd "C:\Program Files (x86)\RabbitMQ Server\rabbitmq_server-3.6.0\sbin" +> .\rabbitmq-service.bat remove +> .\rabbitmq-service.bat install +> .\rabbitmq-service.bat start +{% endhighlight %} + +Of course, The path you cd into will depend on the version of RabbitMQ you have installed. diff --git a/src/posts/2016/02/25/you-cant-subscribe-to-ievent-with-nservicebus.md b/src/posts/2016/02/25/you-cant-subscribe-to-ievent-with-nservicebus.md new file mode 100644 index 0000000..059db0e --- /dev/null +++ b/src/posts/2016/02/25/you-cant-subscribe-to-ievent-with-nservicebus.md @@ -0,0 +1,86 @@ +--- +layout: post +title: "You Can't Subscribe to IEvent with NServiceBus" +date: 2016-02-25 15:56:00 +description: "The problems with trying to subscribe to IEvent with NServiceBus" +categories: [programming] +tags: [.net,nservicebus] +--- + +The reason you might want to subscribe to `IEvent` is pretty straightforward. I +wanted to do it for the simple reason of wanting to be able to choose (based +on configuration) whether I wanted to notify anyone of any event that +could be published on my bus. + +In other words, whenever an event of any type was published, I wanted one particular +endpoint to subscribe to that event and then decide whether it should do +anything with it. + +I thought the code to do that should be pretty simple. Since all published +events *should* implement the `IEvent` interface and NServiceBus supports +polymorphism in its message handling, creating a message handler that +implemented `IHandleMessages` should be all I should have needed! + +I was testing this with only partial success and so I thought it was working. + +{% highlight csharp %} +public class EventHandler : IHandleMessages +{ + public void Handle(IEvent message) + { + ... + } +} +{% endhighlight %} + +## My Partial Success +The reason I had partial success was because I was also *explicitly* handling +other events in the same endpoint on a special saga I had setup. So, when +the endpoint started up, it would subscribe to these events. At first, I only +wanted to send notifications for these events anyway, so it all seemed to be +working. My special `IEvent` would work because the `Saga` subscribed this same +endpoint to those few events explicitly. + +## My Full Failure +I realized I had only partially succeeded when I started wanting to send notifications +for other events that were not in that `Saga`. I dug into the issue for a few +hours and finally began to wonder if NServiceBus (or maybe the RabbitMQ transport) +was explicitly ignoring `IEvent` when it setup subscriptions. + +Sure enough, it does. + +To build a list of types to subscribe to, NServiceBus uses the `Conventions` +class in the `NServiceBus.Core` namespace. One of the checks that code does +is to filter the list of potential types with the `IsEventType` method. This +method checks if the `Type` is in a Particular (NServiceBus) DLL. See the +code here: [IsEventType](https://github.com/Particular/NServiceBus/blob/e4bc405509e3b9c3fc91e21a56333bb40ac54a60/src/NServiceBus.Core/Conventions.cs#L154) + +## My Solution +The solution is simple enough. Instead of listening to `IEvent`, listen +to a custom interface that you implement on all of your events. This is probably +more close to what you are trying to do anyway--listen to all events that your +system produces. + +So, I simply created a marker interface called `ICustomEvent`. + +{% highlight csharp %} +public interface ICustomEvent : IEvent { } +{% endhighlight %} + +Then, on all of my bus event classes, instead of implementing `IEvent` directly, +I implemented `ICustomEvent`. + +My handler then looked like this. + +{% highlight csharp %} +public class EventHandler : IHandleMessages +{ + public void Handle(ICustomEvent message) + { + ... + } +} +{% endhighlight %} + +Now, the right subscriptions are all set up and my single handler gets +every single event published by my entire bus. diff --git a/src/posts/2016/03/03/using-nugetpackager-0.5.5-with-gitversiontask-3.4.1.md b/src/posts/2016/03/03/using-nugetpackager-0.5.5-with-gitversiontask-3.4.1.md new file mode 100644 index 0000000..7dd7a73 --- /dev/null +++ b/src/posts/2016/03/03/using-nugetpackager-0.5.5-with-gitversiontask-3.4.1.md @@ -0,0 +1,74 @@ +--- +layout: post +title: "Using NuGetPackager 0.5.5 with GitVersionTask 3.4.1" +date: 2016-03-03 15:04:00 +description: "A quick fix to use NuGetPackager 0.5.5 with GitVersionTask 3.4.1" +categories: [programming] +tags: [nuget,git] +--- +I'm using both [NuGetPackager](https://www.nuget.org/packages/NuGetPackager/) +with [GitVersionTask](https://www.nuget.org/packages/GitVersionTask/) on +my project that provides EntityFramework persisters for NServiceBus: +[GoodlyFere.NServiceBus.EntityFramework](https://github.com/benjaminramey/GoodlyFere.NServiceBus.EntityFramework). + +NuGetPackager seems to work seamlessly with GitVersionTask 2.0.0. However, +GitVersionTask is up to version 3.4.1 and I just hate sitting at a version that +far behind! + +So, I updated my NuGet package to the latest version of GitVersionTask. That's +3.4.1 right now. Too bad, so sad! That caused this error: + +{% highlight xml %} +The "CreatePackages" task was not given a value for the required parameter "Version". 1 +{% endhighlight %} + +## The Problem +The problem turns out that NuGetPackager 0.5.5 is looking for a MSBuild property +called "GfvNuGetVersion". See the code in the .targets file [here](https://github.com/Particular/NuGetPackager/blob/0.5.6/src/NuGetPackager/NuGetPackager.targets#L5). + +GitVersionTask 3.4.1 (somewhere in a release since 2.0.0) changed that property +name to "GitVersion_NuGetVersion". See the code [here](https://github.com/GitTools/GitVersion/blob/master/src/GitVersionTask/NugetAssets/GitVersionTask.targets#L68). + +## The Solution +So, how to fix this? NuGetPackager will probably be updated some day, but for +now you can just translate the property in your .csproj file before the +CreatePackage task is called in NuGetPackager. + +Open up your .csproj file directly in some text editor like Notepad++. Find +these two lines at the bottom of the file: + +{% highlight xml %} + + +{% endhighlight %} + +Before those two lines, add a target and a property group. The target will +create a task that translates the new property to the new property. The +property group wil define a BuildDependsOn to call that target before the +CreatePackages target is called. + +{% highlight xml %} + + + + + + + + $(BuildDependsOn); + TranslateNugetVersion + + + + +{% endhighlight %} + +Now, when you build your project, everything should work great! diff --git a/src/posts/2016/04/11/owin-oauth-integration-testing-with-bearer-token.md b/src/posts/2016/04/11/owin-oauth-integration-testing-with-bearer-token.md new file mode 100644 index 0000000..e87358d --- /dev/null +++ b/src/posts/2016/04/11/owin-oauth-integration-testing-with-bearer-token.md @@ -0,0 +1,92 @@ +--- +layout: post +title: "OWIN Integration Testing with OAuth Bearer Tokens" +date: 2016-04-11 13:34:00 +description: "How to generate an OAuth bearer token to use with OWIN integration tests." +categories: [programming] +tags: [owin,.net] +--- + +## Situation +The situation is pretty simple. We have a project written with Web API 2.2 +that we have running with OWIN. The API requires user authorization. To +accomplish that, we use the OWIN OAuth libraries. + +Upon until recently, the OAuth authorization server (the thing that takes in +the user credentials and issues the token) and the API (the resource server) were +hosted in the same site and web project. + +Integration testing was pretty easy at this point. For the authorized calls, +we simply wrote helper methods that logged a test user in through the authorization +server (hosted along with the API in the OWIN test server), got a bearer token +and then made the integration test calls. + +## Problem +When we recently decided to split the authorization server and resource server +code up into two separate code bases (so that they could be hosted separately) +we ran into issues with the authorization and API calls no longer being possible within a +single integration test. The code to generate the bearer token was now no +longer accessible in the test server for the API integration tests because the authorization +server was now designed to be hosted separately. We'd have +to somehow generate the bearer token without making a call with the OWIN test +server. + +## Solution +The solution is to generate a valid bearer token in your test code and send it +along with your test API call as you would have previous. How to generate that +valid bearer token though?? It turns out to be easier than I thought. + +How you use OWIN's test server is beyond the scope of this blog post, but +eventually you'll have some code resembling this: + + var server = TestServer.Create(); + +You'll have to use a overload of the `Create()` method to capture a reference +to the server's `IDataProtector`. This interface defines what OWIN OAuth will +use to encrypt a `ClaimsIdentity` into a valid bearer token. + +Update your server creation code like this: + +{% highlight csharp %} + var dataProtector; + var server = TestServer.Create(app => + { + var s = new Startup(); + s.Configuration(app); + + dataProtector = app.CreateDataProtector( + typeof(OAuthBearerAuthenticationMiddleware).Namespace, + "Access_Token", "v1"); + }); +{% endhighlight %} + +You have to get the `dataProtector` this way because it's the same way that +the OWIN OAuth libraries get it before trying to decrypt the bearer token. +See the source for the OWIN OAuth code here: +[OAuthBearerAuthenticationMiddleware.cs](http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs) + +Once you've "captured" the `dataProtector`, in your integration test (or in some +helper code, probably) you can generate your `ClaimsIdentity` and then your +bearer token with the `dataProtector`. + +{% highlight csharp %} + // inside an integration test + using (server) + { + // create your valid identity any way you need to here + ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity("username")); + + // these classes are defined in the OWIN OAuth libraries. + var properties = new AuthenticationProperties(); + var ticket = new AuthenticationTicket(identity, properties); + var format = new TicketDataFormat(dataProtector); + + string bearerToken = format.Protect(ticket); + + // ... call your API through the test server with the bearer token... + var response = server.CreateRequest("/api/someendpoint") + .AddHeader("Authorization", "Bearer " + bearerToken) + .GetAsync() + .Result; + } +{% endhighlight %} diff --git a/src/posts/2016/08/24/castle-windsor-typedfactoryfacility-and-property-injection.md b/src/posts/2016/08/24/castle-windsor-typedfactoryfacility-and-property-injection.md new file mode 100644 index 0000000..479e6ee --- /dev/null +++ b/src/posts/2016/08/24/castle-windsor-typedfactoryfacility-and-property-injection.md @@ -0,0 +1,133 @@ +--- +layout: post +title: "Castle.Windsor TypedFactoryFacility and Unexpected Property Injection Behavior" +date: 2016-08-24 12:37:00 +description: "Interesting (and unexpected) behavior of Windsor's property injection when using TypedFactoryFacility." +categories: [programming] +tags: [.net,castle-windsor] +--- + +I finally got to the bottom of an issue today that came down to the way Castle.Windsor +does property injection (injecting dependencies into public properties) when +you are also using their TypedFactoryFacility. It was a tough one to track down, so +I thought I'd put the solution out there for everyone else's benefit. + +## The Problem +Here's what the problem looked like. + +### The Error Message +First things first, this is what the symptom looked like: + +{% highlight text %} +[ComponentNotFoundException: No component for supporting the service System.Threading.Tasks.Task was found] + Castle.MicroKernel.DefaultKernel.Castle.MicroKernel.IKernelInternal.Resolve(Type service, IDictionary arguments, IReleasePolicy policy) +120 + Castle.Facilities.TypedFactory.Internal.TypedFactoryInterceptor.Resolve(IInvocation invocation) +147 + Castle.Facilities.TypedFactory.Internal.TypedFactoryInterceptor.Intercept(IInvocation invocation) +123 + Castle.DynamicProxy.AbstractInvocation.Proceed() +448 + Castle.Proxies.Func`2Proxy.Invoke(OAuthMatchEndpointContext arg) +168 + + ... +{% endhighlight %} + +This is actually a pretty standard error when you're using Castle.Windsor. It usually +means Windsor tried to resolve some dependency but discovered that it was missing a +registration. Translation: you forgot to register a component or type in your Windsor +setup. Usually it's an easy fix; just register your dependency properly. + +In this case, however, as you can see, the missing registration was of type +System.Threading.Tasks.Task. I didn't have any dependencies on the `Task` class and so +I didn't want to "fix" any Windsor setup to register it. This wasn't making sense. + +### Step One: TypedFactoryInterceptor? +The first oddity I noticed was that this error was happening somewhere in the flow +of the TypedFactoryInterceptor. I could tell where the error was coming from in my +code (from the rest of the stack trace that I didn't include above). The problem +was that it had nothing to do with any TypedFactoryFacility. + +So, I inspected my registrations a little closer. I basically had a setup like this. + +{% highlight csharp %} +public class ChildClass : ParentClass +{ + public override Task DoSomething(Options options) + { + ... + } +} + +public class ParentClass : IInterface +{ + public Func OnDoSomething { get; set; } + public Func OnDoSomethingElse { get; set; } + + public virtual Task DoSomething(Options options) + { + return OnDoSomething(options); + } + + public virtual Task DoSomethingElse(Options options) + { + return OnDoSomethingElse.Invoke(options); + } +} +{% endhighlight %} + +Notice a couple of things. + 1. ParentClass has several virtual methods. I'm only overriding one. + 2. ParentClass is setup to allow you to override the virtual methods or to just supply + it with Funcs. + 3. The implementation of the virtual methods is to simply call the corresponding Funcs. + +### Finding the Location of the Problem +After lots of testing and debugging, I finally figured out that somehow a FuncProxy +(see line 6 in the error stack trace above) was being injected into all of the +public `Func<,>` properties on ParentClass. Then, when methods were called that +I did not override in my ChildClass, the Func would be called in the ParentClass +and that's where Windsor was hooking in and eventually causing an error when it +eventually tried to resolve a dependency on `Task`. + +### One More Clue +As I mentioned above, the presence of a TypedFactoryInterceptor in the stack trace +was strange to me. I wasn't using Windor's TypedFactoryFacility to register my ChildClass. +So, why was it sticking its nose into my class somehow? + +I did some simple debugging and turned off the TypedFactoryFacility in my Windsor setup. +Just like magic, +the error I was seeing went away. So, my conclusion: somehow TypedFactoryFacility was +inserting itself into my ChildClass registration and injecting `FuncProxy`s into the +ParentClass public `Func<,>` properties. + +## The Solution +This looked an aweful lot like property injection to me. I started searching related to +TypedFactoryFacility and property injection. I pretty quickly came upon this documentation: +[https://github.com/castleproject/Windsor/blob/master/docs/how-properties-are-injected.md](https://github.com/castleproject/Windsor/blob/master/docs/how-properties-are-injected.md). + +That page describes how Windsor does property injection, which I actually thought was +an opt-in feature of Windsor. In fact, Windsor does it by default, as describe on that page. + +Combining this revelation (to me) with the fact that TypedFactoryFacility also allows you +to take a dependency on a simple `Func` and turn it into a factory call into your Windsor +container, I concluded that, by turning on the TypedFactoryFacility in Windsor, I was opening +up public properties that had `Func<>` types to property injection by Windsor. + +Now, I hate property injection, so I was fine with just turning it off completely. Luckily, +the documentation link above also included instructions how to do it. I've included that code +here. + +{% highlight csharp %} +var propInjector = Kernel.ComponentModelBuilder + .Contributors + .OfType() + .Single(); +Kernel.ComponentModelBuilder.RemoveContributor(propInjector); +{% endhighlight %} + +I added this code into my Windsor container setup and tried again. Everything worked just +as expected. + +## Lessons Learned +A couple of quick takeaways: + 1. Windsor does property injection by default. Check out the documentation link above for + the criteria it uses to determine when it should or not. + 2. Using TypedFactoryFacility is fantastic, but turn off property injection if you have + public `Func<>` properties anywhere. diff --git a/src/posts/2017/01/20/I-Like-Languages-That-Expect-Me-To-Be-a-Great-Programmer.md b/src/posts/2017/01/20/I-Like-Languages-That-Expect-Me-To-Be-a-Great-Programmer.md new file mode 100644 index 0000000..e000ca9 --- /dev/null +++ b/src/posts/2017/01/20/I-Like-Languages-That-Expect-Me-To-Be-a-Great-Programmer.md @@ -0,0 +1,40 @@ +--- +layout: post +title: "I Like Languages That Expect Me To Be a Great Programmer" +date: 2017-01-20 20:05:00 +description: "Languages that are getting too 'safe'!" +categories: [programming] +tags: [] +--- + +A co-worker of mine recently turned me on to Uncle Bob's blog. Yup, that's Uncle Bob Martin. + +Here's one of his latest posts that sparked a thought in me: [The Dark Path](http://blog.cleancoder.com/uncle-bob/2017/01/11/TheDarkPath.html). + +Make sure to read his follow-up post too: [Types and Tests](http://blog.cleancoder.com/uncle-bob/2017/01/13/TypesAndTests.html). + +When I got done reading those posts, this was my simple thought: I like programming languages +that expect me to be a great programmer. + +Think of anyone you know who is *great* at something. Is it an artist? A politician? A teacher? +I just watched _American Sniper_ so I'm thinking of someone like Chris Kyle. He was a great +sniper. His skill with a rifle was amazing. + +Now, think of why that person is considered great at what they do. Is it because what they +do is easy? Probably not. It's probably because what they do is extremely difficult, but +they still do it well. Or, it may be something that's easy to do, but very difficult to +do *really* well. Think of shooting a rifle. It's very easy to shoot a rifle. Make sure it's +loaded. Aim it somewhere (safe). Pull the trigger. That's it. But how about hitting a target +100 yards away? How about 1000 yards? Yeah, that gets really hard. Only a *great* +marksman could do that. + +The reason it's hard to be a great marksman is because of the flexibility of a rifle. It's +relatively simple design lets you take it almost anywhere and lets you aim it at almost anything. +The only way to make a rifle that even a novice could use to hit targets 1000 yards away +would be to mount it on some kind of stand, add advanced aiming software somehow and let it +pull the trigger. But, that makes it hard to use, doesn't it? You now at the least have a bulky tripod to +lug around a need for a power source for the computer to aim the thing. + +That's how overly restrictive programming languages have become for the sake of safety. They're +getting unwieldy. I want a flexible language that may be harder to use when I'm a novice, but +once I'm an expert, it lets me do amazing things. diff --git a/src/posts/2020/05/12/psalm-130.md b/src/posts/2020/05/12/psalm-130.md new file mode 100644 index 0000000..68cb4f7 --- /dev/null +++ b/src/posts/2020/05/12/psalm-130.md @@ -0,0 +1,147 @@ +--- +layout: post +title: "Psalm 130" +date: 2020-05-12 +description: "Psalm 130 is a plan for returning your thoughts to the Lord from being in the depths." +categories: [scripture] +tags: [psalms] +--- + +## Introduction +Psalm 130 records the thoughts of a believer who goes through a remarkable transformation: from the depths of despair to proclaiming hope in the Lord to his nation. + +How does this transformation happen? + +This is not a psalm about a unique event or a special tragedy or an especially remarkable man accomplishing a gargantuan feat that no one can again accomplish. This is not a super-Christian battling forces of darkness with fantastic spiritual super powers. + +No, instead, it is the straightforward revelation of a man's internal thoughts as they transform from deep darkness to light; of despair to hope for the future; of desperation for his own rescue to confidence that others can be rescued as well. + +What we see in this psalm are the thought patterns of a mature believer; patterns we ought to emulate and repeat in our own lives. + +Now, when I say "mature believer", I want to make sure you've got the same picture that I do. I don't mean someone who "has it all figured out". I definitely don't mean someone who has reached some "higher spiritual plane". All I mean is someone who has walked with the Lord for a long time and has trained his thoughts in the patterns we'll see. He falls. He fails. He's done it before and he's experienced enough to recognize it and know how to get back up again. + +If we copy these patterns of thought, they will drive us to strengthen our upward confidence in the Lord and then mature us to expand our thoughts outward toward our fellow men. But, it begins where it must: our inward thoughts, thoughts directed toward ourselves. + +## Inward +Verses 1-2. + +The author of this psalm begins by revealing the state of his mind: his inward thoughts. He uses only two words to do this: "the depths". + +### He is in trouble +By themselves "the depths" might conjure up enough for us to imagine that he is in some kind of trouble. He's not feeling good. But, this word isn't just a word for how deep a hole might be. Everywhere else it's used in Scripture, it's used to describe deep waters and especially the depths of the sea. That's another kind of "depths" altogether. + +If you've ever taken a swim you can begin to imagine the analogy he is painting. You've probably felt the slight panic to get back to the surface when you stayed under the water a little too long. You've perhaps felt the water pressure when you dove down to the deep end of the pool. + +But, the swimming pool is nothing like the depths of the sea. When you get down deep, the light disappears. The sunlight fades until there is only utter blackness. If help was on the way to pull you out, you couldn't see it coming. You can't even see your hand in front of your face. You're surrounded by darkness. There are no waypoints. No landmarks to recognize. No signs to point the way of escape. + +All comfort of heat vanishes the deeper you sink. Before much depth, the cold becomes unbearable. + +As the waters above increase, the pressure mounts until it would easily crush you. + +Then, of course, you cannot breath. In the depths of the sea you are far from the surface, far from breath. You will suffocate in a matter of minutes--certainly before you make it to the top on your own. + +Darkness, cold, pressure, suffocation--it paints a desperate, hopeless, life-threatening situation. This man is in great trouble. + +### He admits he's in trouble +But, notice the first thought pattern we can recognize of a mature believer. It's the one important thing he does first: he admits he's in trouble. There's no denial here. There's no trying to make the situation not seem so bleak. He's in trouble he cannot escape. He knows it and he says so. This is the first thing we can note as the sign of a mature believer. The mature believer makes honest confession of his desperate need. He frankly admits his trouble and makes no attempt to hide it from himself or from the Lord. + +### He calls out to the Lord +The second thought pattern of a mature believer is quickly noticed next. Admitting his situation has freed him ask for help. + +What would you do if you were sinking, with no one in sight to save you? + +I think my first instinct would be to call out as loudly as I could for someone--anyone at all--to come rescue me! + +My second oldest, Gideon, is not quite able to dress himself yet. When he gets stuck in his shirt or can't quite work the button on his pants, he has the habit of desperately calling out "Somebody! Anybody!". He doesn't care if me or Grace or even his brother (sometimes) helps him. He just needs someone to get this shirt off! + +But, this is not exactly what he does here. He cries out. But he specifically calls out to only one person: the Lord. And there's no doubt who he means. He uses the name of the Lord, Yahweh. He pleads with Him to hear his voice, to hear the voice of his supplications; or, "pleas for mercy" as the ESV translates it. + +It seems counterintuitive to call out for a specific person. If he's truly desperate wouldn't any help be welcome, no matter who it came from? But, this too is a thought pattern of a mature believer. He knows the One who not only can save him, but is willing to save him. There is only One such person and He is the Lord. + +He is the one who will put it on the heart of your spouse to say the right kind word at the right time. Or burden a preacher with an exhortation from the Word that encourages you. Or embolden your brother to give you the strong admonition you need. + +An interesting thing happens next. From a dire situation, his thoughts begin to shift. His calling out to the Lord has shifted his focus from his circumstances to the Lord Himself. He now begins to drift away from his inward thoughts and toward upward thoughts. + +## Upward +Verses 3-6. + +Notice how his thoughts are still on himself, but no longer inward. His thoughts have turned to who he is in light of his relationship to a holy God. They have turned upward. + +### Sin and Forgiveness +What would it be like if the Lord marked our iniquities? What if he kept a record and closely watched them? It's obvious--our case would be hopeless! We'd have no chance of standing at that judgment. + +Think of the things this reveals about his view of God in relation to himself: +- First, he knows he has sinned and does not hide it +- He knows his sin is serious, and it has fearful consequences +- He also knows the Lord knows these sins +- Beyond this, he knows he ought to be rightfully accountable to the Lord for these sins +- But, he also believes that the Lord does not keep a record of these sins +- More than that, he knows it's not a matter of the Lord misplacing the official sin record, but the marks are gone because there is forgiveness with the Lord +- As a result of this forgiveness, he fears the Lord + +So, we can notice the third and fourth thought patterns of a mature believer. + +First, he knows he is a sinner, forgiven by a loving God. This is how he stands, even though he has sinned. This is both a humbling admission and a strengthening encouragement and they are in a kind of balance. Thinking only of ourselves as sinners neglects the truth of God's work to forgive us. Thinking only of the forgiveness we have in the Lord neglects the truth of our sinfulness that necessitated His work in the first place. + +Second, rather than a license to sin, this forgiveness leads him to fall fearfully before the Lord in humble worship. Perhaps, like me, you don't normally associate God's forgiveness with fearing Him. I, for one, am much more likely to associate it with relief or gratefulness or happiness. + +But the connection is perhaps closer than we think. When we acknowledge that God is the one who forgives our sin, at the same time, we're acknowledging that He has the authority and the power to do so. The natural reaction to such a thought is to fear, follow and worship such a God who wields such power over us, but choses instead to forgive us. + +The forgiveness of the Lord also frees a man to leave the things behind him (his sins, weaknesses and failures) and press on in the work of the Lord. In other words, it leads to a greater desire to follow the Lord. As we follow Him, we know Him better. As we know Him better, we can only fear Him more. This certainly seemed to be Paul's mindset in Philippians 3: + +> **Philippians 3:12–17** +> +> 12 Not that I have already obtained it or have already become perfect, but I press on so that I may lay hold of that for which also I was laid hold of by Christ Jesus. +> 13 Brethren, I do not regard myself as having laid hold of it yet; but one thing I do: forgetting what lies behind and reaching forward to what lies ahead, +> 14 I press on toward the goal for the prize of the upward call of God in Christ Jesus. +> 15 Let us therefore, as many as are perfect, have this attitude; and if in anything you have a different attitude, God will reveal that also to you; +> 16 however, let us keep living by that same standard to which we have attained. +> 17 Brethren, join in following my example, and observe those who walk according to the pattern you have in us. + +Notice the connection and progression from leaving what lays behind to striving for the upward call to keeping an attitude of holy living. + +### Waiting and Hoping +Earlier, in verses 1-2, the psalmist honestly admits his circumstances and this leads him to action: to call out to the Lord for rescue. Here too, having turned his thoughts upward to the Lord's forgiveness of his sins, he now takes action. He trains his thoughts on waiting for the Lord and hoping in His word. + +There are at least two things to notice in these two verses. First, the repetition and second, the comparison to the watchmen. + +It's interesting how he repeats, to himself, how he will wait for the Lord. It's almost as if he needs more than one reminder to wait for Him. Perhaps he is retraining his thoughts away from fearfulness and despair to an expectant, waiting hopefulness. + +Keep in mind that we have no indication that his circumstances have changed at all. But his thinking is brightening considerably. His concentration is no longer on his circumstances, but on the Lord who certainly will rescue him. + +Second, we should ask ourselves what the waiting of a watchman for the morning is like. Even for a new watchman, on his first day on the job, I can't imagine he has any question or uncertainty about the morning coming! There is a confidence that the sun will rise and the watch will end. + +The fifth thought pattern of a mature believer now emerges. He trains his thoughts to confidently hope in the Lord, waiting on His rescue, no matter the circumstances. + +As before, now another interesting things happens. As he moves away from inward thinking to upward thinking, he grows in his confidence to trust the Lord as he remembers the lovingkindness of the Lord. In that confidence his thoughts now turn completely away from himself and outward, towards his fellow countrymen. + +## Outward +Verses 7-8. + +Now, the mood of the psalmist has completely changed from where he started. Far from crying out in desperation for help, he is now calling out to his countrymen to hope in the Lord! + +I think we can notice two more thought patterns of the psalmist to emulate. + +### Seeing the needs of others +The first is a sense of responsibility to share his confidence in the Lord with others. +His thinking has been so transformed that, though we still have no indication that he has been rescued, he sees the need of his fellow man for the same trusting hope in the Lord. His own issues are no longer the only ones he sees. Others are in similar circumstances, similar "depths". So, he calls on them to hope based on two characteristics of the Lord: His lovingkindness and his abundant redemption. + +### Confidence in the future +The second is a confidence in the future actions of the Lord. + +How does he know that God will redeem Israel from all his iniquities? + +I think the answer is relatively simple. He has seen the past actions of the Lord in his life. He has observed the character of the Lord worked out in his own life. Having drawn closer to the Lord, he knows Him better and can look out into the future, confident that the Lord will do the same then as He has done in the past. + +## Conclusion +To summarize this psalm, I think we could say this. The mature believer is not someone who has reached a higher plane of Christian living, never finding himself in the depths, but always thinking upward and outward. I'm confident that every believer finds himself in the depths at some time or another and most likely even quite regularly. + +Remember, the one writing this psalm, who I see as a very mature believer, is one who has fallen! But remember what Proverbs 24:16 says: + +> **Proverbs 24:16** +> +> 16 For a righteous man falls seven times, and rises again, But the wicked stumble in time of calamity. + +Rising again is what the mature believer does. But how? That is what this Psalm spells out for us. + +The mature believer is the one whose has learned certain patterns of thought to a consistent way of thinking that does one simple thing: it always turns him to the Lord for rescue. This then enables him to grow his thoughts from necessary introspection (those inward thoughts), upward, to the joy of the reality of his relationship with the Lord. It also emboldens him to reach outward to share with those around him the bounty of hope and lovingkindness available in the Lord. diff --git a/src/posts/2020/05/27/iterating-all-catalog-metaobjects.md b/src/posts/2020/05/27/iterating-all-catalog-metaobjects.md new file mode 100644 index 0000000..e50a15b --- /dev/null +++ b/src/posts/2020/05/27/iterating-all-catalog-metaobjects.md @@ -0,0 +1,462 @@ +--- +layout: post +title: "Iterating All EPiServer Catalog MetaObjects" +date: 2020-05-27 +description: "C# iterators to walk through all EPiServer catalog node and entry content meta objects." +categories: [programming] +tags: [episerver,csharp,.net] +--- + +If you're maintaining an e-commerce-enabled site in EPiServer and you're using the built-in catalog, then +you have probably found the need to incorporate external data into the catalog, perhaps from various sources. + +Such is the case for me on an EPiServer site I've been working on over the last (almost) four years. The primary +data for the catalog comes into EPiServer from an external PIM system. But, then there is all sorts of ancillary +data that needs to calculated from existing fields or pulled from external sources. These external data sources +don't really provide a way to incrementally apply deltas for the data they provide. The only option to make updates is to walk +through the entire catalog and see if anything needs updating when matched against the external data I've pulled. + +This external data is stored in the catalog as custom EPiServer MetaFields. So, +a need arose to easily walk through the entire catalog, pick up every MetaObject (to update the custom MetaFields) +in every available catalog culture and make the appropriate updates. + +Below are the iterators I wrote to accomplish this. They let you walk the entire catalog tree (in a breadth-first manner) +starting from the top as if you were iterating a simple C# array or list. + +## High-Level Usage +Because all we're doing is writing iterators, the high-level usage looks just like you were interating through +an `IEnumerable` in C# like an array or a `List`. + +The iterators are implemented as extension methods on the `ICatalogSystem` interface. So, iterating through all +of the catalog entry MetaObjects looks like this. + +{% highlight csharp %} +using Mediachase.Commerce.Catalog; +using Mediachase.MetaDataPlus; + +ICatalogSystem catalog = CatalogContext.Current; +MetaDataContext mdc = MetaDataContext.Instance; + +foreach ((MetaObject metaObject, MetaDataContext metaDataContext) entryMO in catalog.AllCatalogSystemEntryMetaObjects(mdc)) +{ + entryMO.metaObject["SomeCustomMetaFieldName"] = "external data source value here"; + entryMO.metaObject.AcceptChanges(entryMO.metaDataContext); +} +{% endhighlight %} + +I implemented the separate iterators for NodeContent MetaObjects and the EntryContent +MetaObjects. You easily use them in combination to iterate through the MetaObjects of the entire catalog tree. + +Note a couple of things from the example code above: + 1. You have to give the iterator the MetaDataContext. As we'll see in the iterator code, it will copy this MDC for each catalog language so that you get a MetaObject for every culture present in your catalog. + 1. Because of #1 above, the current MetaDataContext is passed to you during iteration so that you know which catalog culture this MetaObject is for. + +## Extension Method Code +The extension method code for AllCatalogSystemEntryMetaObjects is pretty simple. It uses four iterators in nested foreach loops to loop through + 1. All catalogs + 1. All nodes + 1. All entries + 1. All meta objects + +{% highlight csharp %} +public static IEnumerable<(MetaObject metaObject, MetaDataContext metaDataContext)> AllCatalogSystemEntryMetaObjects( + this ICatalogSystem catalogSystem, + MetaDataContext metaDataContext) +{ + foreach (var catalog in new CatalogSystemCatalogs(catalogSystem)) + { + foreach (var node in new CatalogSystemNodes(catalogSystem, catalog.CatalogId)) + { + foreach (var entry in new CatalogSystemEntries(catalogSystem, catalog.CatalogId, node.CatalogNodeId)) + { + foreach (var tuple in new CatalogSystemEntryMetaObjects(metaDataContext, catalog, entry)) + { + yield return tuple; + } + } + } + } +} +{% endhighlight %} + +I broke the iteration down into separate iterators in this fashion so that I could easily iterate through all the entries (for example) in a single node, if I knew from the beginning which node I wanted to start at. If that was the case, I could easily write another extension method that took a node ID (or node code) as a parameter, use the ICatalogSystem to find that node and then pass the node ID to the CatalogSystemEntries iterator to iterate just the entries under that node. + +## Iterators +We have four iterators in use: + 1. CatalogSystemCatalogs - iterates through all catalogs + 1. CatalogSystemNodes - iterates through all nodes, in a breadth-first fashion, in a catalog + 1. CatalogSystemEntries - iterates through all entries in a node + 1. CatalogSystemEntryMetaObjects - iterates through all MetaObjects for an entry + +### CatalogSystemCatalogs Iterator +The code is pretty simple. We implement an IEnumerable which uses our implementation of an IEnumerator. + +The IEnumerator simply gets all the catalogs from the ICatalogSystem and, as you call MoveNext() (usually done for you by the foreach loop), increments an index value to give you the current CatalogDto.CatalogRow to work with. + +{% highlight csharp %} +public class CatalogSystemCatalogs : IEnumerable +{ + private readonly CatalogSystemCatalogEnumerator _enumerator; + + public CatalogSystemCatalogs(ICatalogSystem catalogSystem) + { + _enumerator = new CatalogSystemCatalogEnumerator(catalogSystem); + } + + public IEnumerator GetEnumerator() => _enumerator; + + IEnumerator IEnumerable.GetEnumerator() => _enumerator; +} + +public class CatalogSystemCatalogEnumerator : IEnumerator +{ + private int _currentIndex = 0; + private readonly ICatalogSystem _catalogSystem; + private Lazy> _catalogs; + + public CatalogSystemCatalogEnumerator(ICatalogSystem catalogSystem) + { + _catalogSystem = catalogSystem; + _catalogs = new Lazy>(GetCatalogs); + } + + public CatalogDto.CatalogRow Current + { + get + { + return _catalogs.Value[_currentIndex]; + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + Reset(); + } + + public bool MoveNext() + { + if (_catalogs.IsValueCreated) + { + _currentIndex++; + } + + if (_currentIndex >= _catalogs.Value.Count) + { + return false; + } + + return true; + } + + public void Reset() + { + _catalogs = new Lazy>(GetCatalogs); + _currentIndex = 0; + } + + private Lst GetCatalogs() + => _catalogSystem.GetCatalogDto().Catalog.Freeze(); +} +{% endhighlight %} + +### CatalogSystemNodes Iterator +For the nodes iterator, things get a little more complicated. Nodes can be nested, so we have tree structure we have to navigate. We use a Queue to walk through the nodes in a breadth-first fashion. + +I like to use as many functional programming techniques as I can in C# these days. I use the great [LanguageExt](https://github.com/louthy/language-ext) library for this. This library is where the `Que`, `Option` and `Lst` data structures come from. I don't want to go in depth about what they are, but to understand the code below, you should now this: `Que` is an immutable `Queue`, `Lst` is an immutable `List` and `Option` is basically the functional way to do the null-object pattern (instead of dealing with nulls, use an object to represent "I don't have a value"). + +If you're not familiar with breadth-first tree traversals, it's not too complicated. What we do first is get all the nodes at the top level (in our case, all the nodes that are direct children of the catalog we pass in) and put them in the queue. Then, we iterate through each of the nodes we put in the queue and add their child nodes to the queue. We do this until we run out of nodes to process in the queue which indicates we've gone through all nodes in the tree. + +{% highlight csharp %} +public class CatalogSystemNodes : IEnumerable +{ + private readonly CatalogSystemNodeEnumerator _enumerator; + + public CatalogSystemNodes(ICatalogSystem catalogSystem, int catalogId, int startingParentNodeId = 0) + { + _enumerator = new CatalogSystemNodeEnumerator(catalogSystem, catalogId, startingParentNodeId); + } + + public IEnumerator GetEnumerator() => _enumerator; + + IEnumerator IEnumerable.GetEnumerator() => _enumerator; +} + +public class CatalogSystemNodeEnumerator : IEnumerator +{ + private Que _nodeQue; + private readonly ICatalogSystem _catalogSystem; + private readonly int _catalogId; + private readonly int _startingParentNodeId; + private Option _current; + + // We have a parameter for the starting node ID (startingParentNodeId) which defaults to 0. If you + // pass in a node ID, we'll start iterating child nodes of that node. If you don't pass a value (keep + // the default of 0), then we'll iterate through all nodes in the catalog. + public CatalogSystemNodeEnumerator(ICatalogSystem catalogSystem, int catalogId, int startingParentNodeId = 0) + { + _catalogSystem = catalogSystem; + _catalogId = catalogId; + _startingParentNodeId = startingParentNodeId; + _nodeQue = Prelude.Queue(_startingParentNodeId); + _current = Option.None; + } + + // ValueUnsafe() lets us get the value of _current (which is an Option) + // or null if the Option has no value. + public CatalogNodeDto.CatalogNodeRow Current => _current.ValueUnsafe(); + + object IEnumerator.Current => Current; + + public void Dispose() + { + Reset(); + } + + public bool MoveNext() + { + // end-case scenario--we've run out of nodes in the queue, so we're + // done traversing the node tree + if (!_nodeQue.Any()) + { + return false; + } + + int nextNodeId = GetNextNodeIdAndUpdateQue(); + + // special starting condition when we want to do the entire tree + // and no other starting node was passed in + if (nextNodeId == 0) + { + // special case of there being a catalog node with no category nodes beneath it + if (!_nodeQue.Any()) + { + return false; + } + nextNodeId = GetNextNodeIdAndUpdateQue(); + } + + _current = GetNode(nextNodeId); + + return _current.IsSome; + } + + private int GetNextNodeIdAndUpdateQue() + { + // get the next node ID from the queue + int nextNodeId = _nodeQue.Peek(); + // remove the retrieved node from the queue + _nodeQue = _nodeQue.Dequeue(); + // get all of this node's children and add their IDs to the queue + _nodeQue = GetChildren(nextNodeId) + .Fold(_nodeQue, (q, nodeRow) => q.Enqueue(nodeRow.CatalogNodeId)); + + return nextNodeId; + } + + private Option GetNode(int nodeId) + => _catalogSystem.GetCatalogNodeDto(nodeId).CatalogNode.HeadOrNone(); + + private Lst GetChildren(int nodeId) + => _catalogSystem.GetCatalogNodesDto(_catalogId, nodeId).CatalogNode.Freeze(); + + public void Reset() + { + _nodeQue = Prelude.Queue(_startingParentNodeId); + _current = Option.None; + } +} +{% endhighlight %} + +### CatalogSystemEntries Iterator +Since entries cannot be arrange in a nested fashion, this iterator is a little less complicated than the nodes iterator. All we do is take in a catalogId and a catalogNodeId and retrieve all the entries below the given node ID, passing them to you one at a time as you call MoveNext(). + +The only mildly complex thing here is that we use a `Lazy` so that we're not retrieving the entries immediately when you instanstiate the IEnumerator. We only load the entries when you MoveNext() and access Current which indicates you really do want some entries. + +{% highlight csharp %} +public class CatalogSystemEntries : IEnumerable +{ + private readonly CatalogSystemEntryEnumerator _enumerator; + + public CatalogSystemEntries(ICatalogSystem catalogSystem, int catalogId, int catalogNodeId) + { + _enumerator = new CatalogSystemEntryEnumerator(catalogSystem, catalogId, catalogNodeId); + } + + public IEnumerator GetEnumerator() => _enumerator; + + IEnumerator IEnumerable.GetEnumerator() => _enumerator; +} + +public class CatalogSystemEntryEnumerator : IEnumerator +{ + private int _currentIndex = 0; + private readonly ICatalogSystem _catalogSystem; + private Lazy> _entries; + private readonly int _catalogNodeId; + private readonly int _catalogId; + + // we take in the catalogNodeId to start at so that you could use this IEnumerator + // to collect entries at any starting node in the catalog. + public CatalogSystemEntryEnumerator(ICatalogSystem catalogSystem, int catalogId, int catalogNodeId) + { + _catalogSystem = catalogSystem; + // we use a Lazy object here so that we don't do a database call + // as soon as you instantiate this class. We only do it once you + // access Current, indicating you really want to iterate the entries now + _entries = new Lazy>(GetEntries); + _catalogNodeId = catalogNodeId; + _catalogId = catalogId; + } + + public CatalogEntryDto.CatalogEntryRow Current + { + get + { + return _entries.Value[_currentIndex]; + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + Reset(); + } + + public bool MoveNext() + { + // the first MoveNext() call will be when when _entries doesn't have + // a value yet (but will after Current is accessed). So, this leaves + // the _currentIndex at 0 the first time MoveNext() is called. + if (_entries.IsValueCreated) + { + _currentIndex++; + } + + // we just incremented _currentIndex to the count of the _entries list, + // so we return false to indicate there are no more values. We really could + // use == here instead of >=. I guess >= feels safer for some reason. + if (_currentIndex >= _entries.Value.Count) + { + return false; + } + + return true; + } + + public void Reset() + { + _entries = new Lazy>(GetEntries); + _currentIndex = 0; + } + + private Lst GetEntries() + => _catalogSystem.GetCatalogEntriesDto(_catalogId, _catalogNodeId).CatalogEntry.Freeze(); +} +{% endhighlight %} + +### CatalogSystemEntryMetaObjects Iterator +The MetaObjects iterator is a different than the other iterators in that it doesn't really iterate through a list or tree of MetaObjects. Instead, it iterates through all fo the MetaObject instances for a particular entry--one instance for reach culture available for the catalog. + +So, as you'll see in the code below, one of the first things we do is grab all of the languages for the catalog so that we know what we need to interate through. + +{% highlight csharp %} +public class CatalogSystemEntryMetaObjects : IEnumerable<(MetaObject metaObject, MetaDataContext metaDataContext)> +{ + private readonly CatalogSystemEntryMetaObjectsEnumerator _enumerator; + + public CatalogSystemEntryMetaObjects(MetaDataContext metaDataContext, CatalogDto.CatalogRow catalog, CatalogEntryDto.CatalogEntryRow entry) + { + _enumerator = new CatalogSystemEntryMetaObjectsEnumerator(metaDataContext, catalog, entry); + } + + public IEnumerator<(MetaObject metaObject, MetaDataContext metaDataContext)> GetEnumerator() => _enumerator; + + IEnumerator IEnumerable.GetEnumerator() => _enumerator; +} + +public class CatalogSystemEntryMetaObjectsEnumerator : IEnumerator<(MetaObject metaObject, MetaDataContext metaDataContext)> +{ + private int _currentIndex = 0; + private readonly CatalogEntryDto.CatalogEntryRow _entry; + private readonly MetaDataContext _metaDataContext; + private Lazy> _catalogLanguages; + private readonly CatalogDto.CatalogRow _catalog; + + public CatalogSystemEntryMetaObjectsEnumerator(MetaDataContext metaDataContext, + CatalogDto.CatalogRow catalog, + CatalogEntryDto.CatalogEntryRow entry) + { + _catalog = catalog; + _entry = entry; + _metaDataContext = metaDataContext; + _catalogLanguages = new Lazy>(GetCatalogLanguages); + } + + public (MetaObject metaObject, MetaDataContext metaDataContext) Current + { + get + { + // what we're really iterating through is the catalog languages. + // as we iterate through them, we grab the associate MetaObject for the current + // language for the given entry + CultureInfo currentCultureInfo = _catalogLanguages.Value[_currentIndex]; + + var mdc = _metaDataContext.Clone(); + mdc.UseCurrentThreadCulture = false; + mdc.Language = currentCultureInfo.Name; + + return (MetaObject.Load(mdc, _entry.CatalogEntryId, _entry.MetaClassId), mdc); + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + Reset(); + } + + public bool MoveNext() + { + // As with the Lazy for iterating through the entries, the first tie MoveNext() is called + // the _catalogLanguages Lazy will not have a value. It will after the first Current access. + // So, for the first MoveNext(), we want to keep the _currentIndex at 0, which is what this if-statement + // accomplishes. + if (_catalogLanguages.IsValueCreated) + { + _currentIndex++; + } + + if (_currentIndex >= _catalogLanguages.Value.Count) + { + return false; + } + + return true; + } + + public void Reset() + { + _catalogLanguages = new Lazy>(GetCatalogLanguages); + _currentIndex = 0; + } + + private Lst GetCatalogLanguages() + => _catalog.GetCatalogLanguageRows() + .Map(lr => new CultureInfo(lr.LanguageCode)) + .Freeze(); +} +{% endhighlight %} + +## Warning +Even though the above code will give you the convenience of iterating through your entire EPiServer catalog(s), be warned! You could easily forget what the IEnumerators really do above and do something like this: + +{% highlight csharp %} +// REALLY BAD CODE, DON'T DO +List<(MetaObject metaObject, MetaDataContext mdc)> allTheMetaObjects = _catalogSystem.AllCatalogSystemEntryMetaObjects(mdc).ToList(); +{% endhighlight %} + +I hope you see the danger of this. If you have a large catalog, you're loading the ENTIRE set of MetaObjects into memory in your `allTheMetaObjects` list. This probably won't be good for your application. If you have a small catalog, this might not be a big deal. But, keep this in mind! diff --git a/src/posts/organize.ps1 b/src/posts/organize.ps1 new file mode 100644 index 0000000..0bbc7f1 --- /dev/null +++ b/src/posts/organize.ps1 @@ -0,0 +1,16 @@ +$posts = gci ./ -filter *.md + +$posts | % { + $filename = $_.Name + $parts = $filename.split("-") + $year = $parts[0] + $month = $parts[1] + $day = $parts[2] + + $filename = $filename -replace "$year-$month-$day-","" + $newDir = "./$year/$month/$day" + + new-item $newDir -itemtype directory -ErrorAction SilentlyContinue + + move-item $_.FullName "$newDir/$filename" +} \ No newline at end of file diff --git a/src/templates/page.js b/src/templates/page.js new file mode 100644 index 0000000..eada32e --- /dev/null +++ b/src/templates/page.js @@ -0,0 +1,13 @@ +import React from "react"; +import Layout from "../components/layout" + +export default function Page({ children }) { + return ( + +
+

page.title

+ {children} +
+
+ ); +} \ No newline at end of file diff --git a/src/templates/post.js b/src/templates/post.js new file mode 100644 index 0000000..7a7ff8d --- /dev/null +++ b/src/templates/post.js @@ -0,0 +1,71 @@ +import React from "react"; +import { graphql } from "gatsby"; +import Layout from "../components/layout"; +import { DiscussionEmbed } from 'disqus-react'; + +export default function Post({ data }) { + const post = data.markdownRemark; + + var tags = post.frontmatter.tags.map((tag) => { + return (#{tag}); + }); + var categories = post.frontmatter.categories.map((category) => { + return (#{category}); + }); + var postDate = new Date(post.frontmatter.date); + console.log(postDate); + var isoDate = postDate.toISOString(); + var visibleDate = postDate.toDateString(); + + return ( + +
+
+

{post.frontmatter.title}

+
+ + + +
+
+
+ + + +
+
+ ) +} + +export const query = graphql` + query($slug: String!) { + markdownRemark(fields: { slug: { eq: $slug } }) { + html + + frontmatter { + title + date + categories + tags + } + } + site { + host + port + } + } +` \ No newline at end of file