[{"content":"Recently I started working on an app to share secrets with other people in a safe way. I have seen and used apps like these before, like secrets.mendix.com. However I wanted to get started with python flask. So I used this as an excuse to do so.\nPython uses a nice library, cryptography that supports two-way encryption using a key.\nSo I googled my ass off, like anyone would do. I am more familiar with PHP and Laravel. I love the principle of managing the db using migrations, so I figured there would be something similar for python flask as well. And guess what. There is! It is SQLAlchemy, which uses albemic under the hood. This supports exactly what I want.\nIt did not take too much effort to get this started. I did struggle a bit with creating and running the migrations. But after a while, I figured out that I needed to create the versions first, using the revision command.\nflask db revision -m \u0026#34;My version\u0026#34; When I finally figured that out, I was good to go. Just run a migration script on deployment, and profit!\n# wsgi_app/migrations/migrate.sh echo \u0026#34;===== Initalize the database =====\u0026#34; # avoid annoying error after first migration if ! [[ -d wsgi_app/migrations ]]; then flask db init else echo \u0026#34;DB already initialized. Skipping\u0026#34; fi echo \u0026#34;===== Run the migrations =====\u0026#34; flask db migrate echo \u0026#34;===== Upgrade the database =====\u0026#34; flask db upgrade So now I have an app, but it looks terrible. How to fix this\u0026hellip; I had heard about tailwindcss. So I thought, maybe I can make it easy on myself, and see if I can use that. And boy, was I not disappointed.\nFirst I needed to integrate tailwind into my application. Unfortunately I had to fallback to npm, but that was a small price to pay.\nI found a tutorial online on how to implement tailwind in a flask project.\nI just followed the tutorial, and it worked. I did notice that compiling the tailwind css, it does some magic below the hood where it checks, the files indicated in your tailwind configuration, for used classes. Only those classes are included in the generated file.\nTo do that, add the content section in your tailwind config.\n// tailwind.config.js module.exports = { darkMode: \u0026#39;class\u0026#39;, content: [ \u0026#39;./wsgi_app/templates/**/*.{html,js,svg}\u0026#39;, ], } To solve that, just make sure that every time that a new class is added in one of the templates, that the tailwind css\u0026rsquo;es are regenerated. Locally, I fixed this by running an npm container with nodemon, watching changes in all my templates.\nI have the following section defined in the package.json\n{ ... \u0026#34;scripts\u0026#34;: { \u0026#34;compile\u0026#34;: \u0026#34;npx tailwindcss -i wsgi_app/static/src/main.tailwind -o wsgi_app/static/css/main.css\u0026#34;, \u0026#34;prewatch\u0026#34;: \u0026#34;npm install\u0026#34;, \u0026#34;watch\u0026#34;: \u0026#34;nodemon --watch wsgi_app/static/src --watch wsgi_app/templates --exec \u0026#39;npm run compile\u0026#39;\u0026#34;, \u0026#34;build\u0026#34;: \u0026#34;npm run compile\u0026#34; }, ... } Now, every time I change a template, the css file is automatically regenerated. 💖\nHappy secret sharing!!\nYou can find the project in github and an example app on rikdegroot.io. And guess what, it has an API 😍\n","id":0,"tag":"development ; tailwindcss ; python ; flask ; sqlalchemy ; lang-en","title":"What about tailwindcss?! I'm hooked!","type":"post","url":"https://rikdegroot.io/-/2022/02/17/what-about-tailwindcss-im-hooked/"},{"content":" Bake your own sourdough from scratch Haha, fooled you. At some point (or never) I will port this presentation I once gave at easee to html. For now, enjoy the pdf export from google slides\n ","id":1,"tag":"lang-en ; baking ; sourdough","title":"How to Make Your Own Sourdough Starter from Scratch","type":"presentation","url":"https://rikdegroot.io/presentations/make-your-own-sourdough-starter/"},{"content":"The Problem Every now and then, for work, I have to provide data to a translation agency, because we need to support what we do in yet another language. It is important that we can reach more users in their native language.\nThis is not always as straightforward as it seems. The application is running multiple services in at least the same amount of tech stacks. In the one repository, our language files are deeply nested JSON files. In yet another repository it\u0026rsquo;s PHP files containing nested associative arrays.\nSo a single request from the translation agency to provide all the English texts sounds simple. But what they mean is\n Can you send an excel spread sheet with the english texts in a column, so we can run it through Google Translate\n They will not be happy with sending them a zip file with PHP and JSON files, with the request to please not f*ck up the keys, because otherwise the implementation on our side is screwed.\nBut the down side of a spreadsheet is the limitation to the amount of dimensions: 2. Also, it is really convinient to persist the keys, because not for every languange there is a native speaking developer at hand, who understands what these translations mean. And matching them back to the keys would be impossible (if we would even want to). And then additionally copy-pasting data ack and forth is not what makes someone happy (some hypocracy here, since that is what we actually do most of the time).\nSo lets solve the first problem of the dimensions by using dot-seperated keys. Basically this means that a nested file is turned in to a flat key: value file, where the nesting is represented by dots in the keys, like so\n{ a: { b: { c: [ 1, \u0026#39;f\u0026#39;, g: { \u0026#39;t\u0026#39; } ] } } } will be turned into\n key value a.b.c.0 1 a.b.c.1 \u0026lsquo;f\u0026rsquo; a.b.c.2.g \u0026lsquo;t\u0026rsquo; Where we just simply use indexes for arrays.\nHaving this, we have something that we can provide to external parties. We leave the english text in for them. As long as they just ignore the keys, all should be fine.\nWhen it is done, they will return something with the reference still to the keys (we explicitly ask them to not remove the column with the dot-seperated keys from the file, so it can be easiy mapped onto the intial values).\nThen all that needs to be done is to unflatten the dot-seperated keys, ans transform it back into the PHP or JSON file.\nThe solution (at least for me; hope it helps\u0026hellip;) So I created a php-json-tool utility for this. Just call it with curl. Provide your file and the output format (check the examples). All data is streamed here, and nothing is stored, nor tracked.\nIf you prefer to do it yourself, feel free to run locally, fork, or provide feedback. The sources are on Gitlab: https://gitlab.com/hwdegroot/php-json-tool\nNow, all that needs to be done, is put the file in the correct location. If not specified so already in the request (using --output in curl).\nStay Safe!\n","id":2,"tag":"development ; lang-en ; i-am-lazy ; automation","title":"Who Still Uses Them CSV Files??!1","type":"post","url":"https://rikdegroot.io/-/2020/12/16/who-still-uses-them-csv-files1/"},{"content":" 640 Kilobytes!!!!1!!1 I shit you not. That is like 10 times the size of Donald Trump\u0026rsquo;s brain.\n Recently I was trying to get my son enthousiastic for programming. At the time of writing he is 7 years old and getting interested in all kinds of electronics, so I thought that getting acquainted with programming would not hurt him. And I like to think of myself as a parent that stimulates his kids, so I used that as an excuse to look into older computers, because nostalgics.\nShow me them footage\nMy kids grew up with LED monitors and TV\u0026rsquo;s and never really saw a real cathode tube, except on the episodes of Pat \u0026amp; Mat. I still remember the soft fading sound of of the tv turning off and the graphics vanishing into this thin line.\nYour browser does not support the video tag. Mesmerizing shutdown. The terminal vanishes into a line. Besides that, I am a fan of clicky keyboards. I have a DasKeyboard 4C ultimate tenkeyless with Cherry Blue switches and a 4C Profressional with brown switches. Sitting at home during the COVID-19 period, made me google old skool stuff a lot.\nSo first I laid my eyes on a IBM Model M2 and got this pretty cheap on the dutch eBay. Getting this to work on my modern laptop was not rocket science, but not straight forward either. I warned my collegues that the quiet days at the office were over. But this also opened up a window into vintage computers and computing. What if I could get a vintage computer, I thought. How awesome would that be?\nHow cool would it be to program a vintage computer with my collegues, or my kids. With all the speed we get nowadays, who still thinks about the limits of computing power. This will be totally different if you have just a fraction of the memory and chip available.\nIBM 5160 or PC XT I am from 1983. So I was looking for a computer from that year. IBM was the company in those days for personal computing and when it came to making PC\u0026rsquo;s (I am NOT an apple fan). So I found that IBM produced the IBM PC XT in that year. I also found out that you could still get them online for a reasonable price. Luckliy I was able to lay my hands on one, in a pretty good state. It came with an IBM Model M keyboard with the silver label (the PC is from 1986). The sound of that is even better than the Model M2.\nYour browser does not support the audio tag. Need I say more... After introducing my kids to th DIR command (it was the only one I was pretty sure about it would work), they wanted to type \u0026ldquo;words\u0026rdquo; on the old computer (first success).\nExiting Vim is hard? So, I know the DIR command. But now what. Let\u0026rsquo;s see what commands are available.\n No tab completion. TAB just places the cursor somewhere down the line No HISTORY. You can repeat the last command by pressing the right-arrow. FEEDBACK\n This is incorrect. You say that you have IBM PC DOS 5. If so, this includes the DOSKEY command. This will give you a command-line history with editing. Just type dos\\doskey to load it.\n For a starters, on IBM DOS (version 5.0) there is no $PATH (or %PATH). The executables are located in C:\\DOS (or c:\\dos, because DOS don\u0026rsquo;t care about casing). the most executables are located. After a day or two I figured this out, so I finally managed to open my first BASIC program. All fine, until I wanted to quit the program. It\u0026rsquo;s not that easy as exiting Vim. It took me quite some time googling, until I finally found this lifesaver.\nFEEDBACK\n There certainly should be! DOS has 2 configuration files, which live in the root directory of the boot drive (A: or C:). They are called [1] CONFIG.SYS and [2] AUTOEXEC.BAT. In the 2nd, there should be a line: PATH=C:\\DOS; C:\\\n Entering BASIC is peanuts \u0026times; Stuck in BASIC \u0026times; Your browser does not support the video tag. Trying to exit QBASIC. Epic fail FEEDBACK\n That is not QBASIC; QBASIC has a GUI. You were in either BASICA or GWBASIC. The command to quit is syst em, if I remember correctly after 30 years.\n So, now I can start a few commands, but getting all available commands is not that straight forward. There is a lot in the DOS directory, but there is no scrolling, and the monitor only is 25 lines.\nFEEDBACK\n Yes there is [scrolling]. Type dir /p for page-by-page. dir /w gives a wide listing. You can combine these: dir /w /p. You can also do dir | more\n [the monitor is only 25 lines] This depends on the graphics card. If you have an MDA card, no, 25 lines is all. Try mode con: lines=43 or mode con: lines=50. This will only work on a VGA-compatible card, though, and you will need ANSI.SYS installed, I think.\n So figuring out the available commands is using a lot of DIR *.EXE's and DIR *.COM's.\nFirst class fun.\nShow me the pics Not so long ago I was explaining my collegue (who is using a screensaver), where a screensaver got its name from. Back in the days, when we were all running the pipes so the screen would not f*ck up .\nBut now, sit back and relax\u0026hellip;\nYour browser does not support the video tag. Check this insane refresh rate of the cathode tube. The color of the terminal is magnificent! 😍 Your browser does not support the video tag. And more refresh rate. The mesmerizing fading away of the fonts into the background. Beautiful, just beautiful Your browser does not support the video tag. 🔈 The startup is amazing as well. The sound of the fan, and the nostalgic beep. Your browser does not support the video tag. 🔈 One more time. I could loop this forever. un DOS tres. The fluorescence is soooo pretty. \u0026times; wppreview, I totally miss the point of this program. But, hey, it's there. \u0026times; FEEDBACK\n It [wppreview] is not part of DOS. Sounds like a WordPerfect preview program for use with mailmerge.\n What next? So far I had to explain to my son what a file(name) and a command is (when they were typing \u0026ldquo;words\u0026rdquo; the IBM kept returning\nBad command or file name So the experience is already educational :)\nTo be honest, I do not have a clear idea what I am going to do with it next. I will be playing with it for a while like an 8 year old with his trains. After the #stayathome is over, hopefully I can take it to the office, so we can start doing real cool things with it.\nI will definitely have to up my GOTO skills :)\nI will start using my Model M2 for work (sorry collegues), for sure. I will have to remap my function key in i3, because I am currently using the windows key for this. But the Model M2 does not have one. But I will overcome.\nFEEDBACK\n It is easy to remap CapsLock to be a “Windows” (Super) key. This is how I use my IBM Model M in Linux. I suggest xmodmap.\n Besides that, I found this great archive with manuals and bootdisks and even PC DOS 5.02. Currently I am trying to get a VM up running PC DOS 5.0 (yes, that is possible in virtualbox)\nFEEDBACK\n If you are willing to change the DOS version, I suggest DR DOS 3.41. The reason is this: MS/PC DOS 5, 6 \u0026amp; later are designed for 386 memory management. This is impossible on an 8088 chip, and as a result, you will have very little free memory. Many DOS programs won’t work.\n DR-DOS is a better 3rd party clone of DOS, by the company that wrote the original OS (CP/M) that MS-DOS was ripped-off from. The first version is 3.41 (before that it had different names) and it is far more memory-efficient. https://winworldpc.com/product/dr-dos/3x\n But if you want to stay with an IBM original DOS, then IBM developed PC DOS all the way to version 7.1, which supports EIDE hard disks over 8GB, FAT32 and some other nice features. It is a free download.\n I have described how to get it here: https://liam-on-linux.livejournal.com/59703.html\n PC DOS 7 is a bit strange; IBM removed Microsoft’s GUI editor and replaced it with an OS/2-derived one called E, which has a weird UI. IBM also removed GWBASIC and replaced it with the Rexx scripting language.\n Personally, I combine bits of PC-DOS 7.1 with Microsoft’s editor, Microsoft’s diagnostics, Scandisk disk-repair tool and some other bits, but that is more than I can cover in a comment!\n There is a lot you can do to upgrade a 5160 if you wish. Here is a crazy example: https://sites.google.com/site/misterzeropage/\n I would not go that far, but a VGA card, VGA CRT, a serial mouse and an XTIDE card with a CF card in it, and it would be a lot easier to use…\n The downside, my Cherry MX blue switches feel like second class now.\nUPDATE When I was installing my VM with PC DOS, at the end of the installation I was aske if I wanted to start in shell mode. It turns out there is a command DOSSHELL (needs to be executed fron C:\\DOS) which gives you a very fancy gui.\n 😱 It has a GUI. \u0026times; UPDATE 2 I recently received some awesome feedback from Liam Proven. If you read through the post there will be updates with the feedback. Thanks for the feedback @Liam.\n","id":3,"tag":"development ; vintage-computing ; ibm-5160 ; lang-en","title":"640 kiloBytes of RAM??! and Why I Bought an IBM 5160","type":"post","url":"https://rikdegroot.io/-/2020/05/19/640-kilobytes-of-ram-and-why-i-bought-an-ibm-5160/"},{"content":"When I was working on a static site, generated with hugo, the amount of pages started to get really out of hand. I was looking for pages, but wasn\u0026rsquo;t entirely sure where to look for them. This was the point that it crossed my mind that searching the site would be extremely convenient.\nSo my first thought was, let\u0026rsquo;s install a search package. However this was not as available as I initially thought. Hugo has some docs on search functionality, however none of them give a full implementation example. This post will.\nThe problem Static sites are generated into all available paths, and then those files are served. The server won\u0026rsquo;t be running a nice database that you can query for some content. This means that the database that will be used for searching has to be generated as well. In this example that database will be a generated json file that will be served over the path /search.\nGetting started Here I will describe the functionality that is available on this site. It will use lunrjs as a client side search engine, that is lightweight and super easy to get started with search library.\nThe source is of the implementation described in this post is available here.\nThe solution is not exactly rocket science, but it took me some time to get it all working and integrated, so hopefully this example will save you some time.\nWhat do we need To get this up and running we are going to need 4 things\n An endpoint that can serve the json database A template that will generate the json file, so we can query it A client side script that retrieves the database file, and allows searching in the file (I will use lunr for that A search page (in this case a partial), so it is actually possible to jot down search words The endpoint If you have a default layout for your hugo site, like I do, you will need to make the /search endpoint available. There are multiple ways to do that, but I chose to make a directory search inside the content directory, containing an index.md file. You can see it here. The index.md file will contain dummy content. You can use this to add some documentation for the team if you\u0026rsquo;d prefer. But really, what\u0026rsquo;s in there doesn\u0026rsquo;t matter, because the actual file won\u0026rsquo;t be served.\nWhat is important is the type of the file. I used data, but it doesn\u0026rsquo;t matter at this point. We will only have to make sure that we use it when we are creating the json template. Make sure to put it in the front matter.\nMine looks like this\n---type:data---Make sure, to exclude the data type from the pages that you want people to see in the list.\nIn my layouts/_default/list.html template that drills down to\n{{ range $index, $element := where .Paginator.Pages \u0026#34;.Type\u0026#34; \u0026#34;==\u0026#34; \u0026#34;post\u0026#34; }} ... {{ end }} because I only want to list pages of type post. But you can change this to\n{{ range $index, $element := where .Paginator.Pages \u0026#34;.Type\u0026#34; \u0026#34;!=\u0026#34; \u0026#34;data\u0026#34; }} ... {{ end }} You see, nothing fancy so far. Now that the /search endpoint is available, it\u0026rsquo;s time to proceed, however hugo will error out on this, because there is no template for this type. So let\u0026rsquo;s do that next.\nCreating the data template Because I chose the type to be data and to use a directory search with a file index.md, I created a directory data in /content/layouts. Inside this directory I put the template single.html. This is the file that hugo expects for a file called index.md. If you prefer _index.md make sure to call this file baseof.html. If you don\u0026rsquo;t want to put the search file inside a directory, but want to add search.md to the root of the content dir, then call this file list.html.\nOnce this is done, you might have to restart your local hugo server, if you are running it locally like me. When that is done, there will be an empty page when you browse http://localhost:8888/search (or whatever port it runs locally).\nBut we want this url to show a nice json representation of all the pages, because that we can load into [lunr][lunr-js].\nFilling the template You can view how to fill the template here\nThe example is based on this gist from goblindegook.\n{{ $.Scratch.Add \u0026#34;index\u0026#34; slice }} {{ $searchablePages := where .Site.Pages \u0026#34;Params.type\u0026#34; \u0026#34;==\u0026#34; \u0026#34;post\u0026#34; }} {{ range $index, $page := $searchablePages }} {{ .Scratch.Set \u0026#34;pageData\u0026#34; \u0026#34;\u0026#34; }} {{ .Scratch.Set \u0026#34;pageContent\u0026#34; \u0026#34;\u0026#34; }} {{ .Scratch.Set \u0026#34;pageURL\u0026#34; \u0026#34;\u0026#34; }} {{ .Scratch.Set \u0026#34;pageTag\u0026#34; \u0026#34;\u0026#34; }} {{ if gt (len $page.Content) 0 }} {{ .Scratch.Set \u0026#34;pageContent\u0026#34; $page.Plain }} {{ .Scratch.Set \u0026#34;pageURL\u0026#34; $page.Permalink }} {{ if (isset $page.Params \u0026#34;tags\u0026#34;) }} {{ .Scratch.Set \u0026#34;pageTag\u0026#34; (delimit $page.Params.tags \u0026#34; ; \u0026#34;) }} {{ end }} {{ .Scratch.Set \u0026#34;pageData\u0026#34; (dict \u0026#34;id\u0026#34; $index \u0026#34;title\u0026#34; $page.Title \u0026#34;url\u0026#34; (.Scratch.Get \u0026#34;pageURL\u0026#34;) \u0026#34;content\u0026#34; (.Scratch.Get \u0026#34;pageContent\u0026#34;) \u0026#34;tag\u0026#34; (.Scratch.Get \u0026#34;pageTag\u0026#34;)) }} {{ $.Scratch.Add \u0026#34;index\u0026#34; (.Scratch.Get \u0026#34;pageData\u0026#34;) }} {{ end }} {{ end }} {{ $.Scratch.Get \u0026#34;index\u0026#34; | jsonify }} You can edit the fields to whatever you like. For convenience I set the id field to the incrementor of the list.\nNow when you visit the /search endpoint, it will return json with the following layout\n[ { \u0026#34;id\u0026#34;: \u0026#34;The id generated by hugo\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;The page title\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;Link to the page, mostly so we can link it from the search results\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;A plain text string of the content\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;semicolon seperated string of the tags, because that makes them searchable\u0026#34; }, ... ] Client side searching Now we are ready to use this in the client. I am loading the json file in a promise, so it won\u0026rsquo;t annoy the user when the file gets huge. I will use axios for this.\nUPDATE: axios has been replcaed by browser\u0026rsquo;s native fetch.\nMake sure that if you do so, and you really can\u0026rsquo;t drop all the IE users, that you will need to polyfill Promise as well.\nI don\u0026rsquo;t care about IE users, so I didn\u0026rsquo;t. Also I was too lazy to setup a webpack config. So you will notice that the javascript syntax is not ES5+, and I will load the libraries from a cdn (unpkg in this case, but there are plenty)\nSo add the following scripts to your base template if you are also lazy\nAxios The axios part is deprecated for my site, but if you chose to use axios, you will need to add this as well.\n\u0026lt;script src=\u0026#34;https://unpkg.com/axios/dist/axios.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; Lunr \u0026lt;script src=\u0026#34;https://unpkg.com/lunr/lunr.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; Polyfill for Promise On polyfill.io you can click the bundle you want, and it will generate a script tag for you. If you do this for Promise only you will get something like this\n\u0026lt;script crossorigin=\u0026#34;anonymous\u0026#34; src=\u0026#34;https://polyfill.io/v3/polyfill.min.js?flags=gated%2Calways\u0026amp;features=Promise\u0026#34; \u0026gt;\u0026lt;/script\u0026gt; If you need more, it is pretty straight-forward. Make sure that you put the polyfill as the first element after \u0026lt;body\u0026gt;.\nI put them all in a partial, that I load in my head (except the polyfill, because, like I said, I don\u0026rsquo;t care).\nSo all set there, time to create a partial for the client side search. You can find the partial here, but it looks like this.\n\u0026lt;div class=\u0026#34;show-search\u0026#34;\u0026gt; \u0026lt;a class=\u0026#34;toggle-search\u0026#34; title=\u0026#34;search across all content\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; width=\u0026#34;612.056\u0026#34; height=\u0026#34;612.057\u0026#34; viewbox=\u0026#34;0 0 613 613\u0026#34;\u0026gt; \u0026lt;path d=\u0026#34;M595.2 513.908L493.775 412.482c26.707-41.727 42.685-91.041 42.685-144.263C536.459 120.085 416.375 0 268.24 0 120.106 0 .021 120.085.021 268.219c0 148.134 120.085 268.22 268.219 268.22 53.222 0 102.537-15.979 144.225-42.686l101.426 101.463c22.454 22.453 58.854 22.453 81.271 0 22.492-22.491 22.492-58.855.038-81.308zm-326.96-54.103c-105.793 0-191.585-85.793-191.585-191.585 0-105.793 85.792-191.585 191.585-191.585s191.585 85.792 191.585 191.585c.001 105.792-85.791 191.585-191.585 191.585z\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;aside role=\u0026#34;search\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;close toggle-search\u0026#34;\u0026gt; \u0026lt;svg height=\u0026#34;512\u0026#34; width=\u0026#34;512\u0026#34; viewbox=\u0026#34;0 0 512 512\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34;\u0026gt; \u0026lt;path d=\u0026#34;M443.6 387.1L312.4 255.4l131.5-130c5.4-5.4 5.4-14.2 0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4-3.7 0-7.2 1.5-9.8 4L256 197.8 124.9 68.3c-2.6-2.6-6.1-4-9.8-4-3.7 0-7.2 1.5-9.8 4L68 105.9c-5.4 5.4-5.4 14.2 0 19.6l131.5 130L68.4 387.1c-2.6 2.6-4.1 6.1-4.1 9.8 0 3.7 1.4 7.2 4.1 9.8l37.4 37.6c2.7 2.7 6.2 4.1 9.8 4.1 3.5 0 7.1-1.3 9.8-4.1L256 313.1l130.7 131.1c2.7 2.7 6.2 4.1 9.8 4.1 3.5 0 7.1-1.3 9.8-4.1l37.4-37.6c2.6-2.6 4.1-6.1 4.1-9.8-.1-3.6-1.6-7.1-4.2-9.7z\u0026#34;/\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;search-wrapper\u0026#34;\u0026gt; \u0026lt;form class=\u0026#34;search\u0026#34; method=\u0026#34;get\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;search\u0026#34; placeholder=\u0026#34;search...\u0026#34; disabled=\u0026#34;disabled\u0026#34; /\u0026gt; \u0026lt;div class=\u0026#34;search-content\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;https://lunrjs.com/guides/searching.html\u0026#34;\u0026gt;Read more on how to search\u0026lt;/a\u0026gt; on the \u0026lt;a href=\u0026#34;https://lunrjs.com\u0026#34;\u0026gt;lunrjs\u0026lt;/a\u0026gt; page \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;ul class=\u0026#34;search-results\u0026#34;\u0026gt; \u0026lt;li\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/aside\u0026gt; {{ $script := resources.Get \u0026#34;js/search.js\u0026#34; | resources.Minify | resources.Fingerprint }} \u0026lt;script src=\u0026#34;{{ $script.RelPermalink }}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; Note that the javascript file is already included here. I will come back to that in the next paragraph\nIt will need some styling. Take a peek here.\nAdditionally, if you put the styling in a scss file, make sure to load it. The template I am using, allows injection of css via an _extra.scss file in the assets/css directory. So I just created a _search.scss file in the assets directory, and included it in the _extra.scss like so\n... @import \u0026#39;search\u0026#39;; ... The real magic Now the final step is to make the search work. For that, just create a javascript file in the assets directory. For me that is assets/js/search.js.\nIt looks like this:\ndocument.addEventListener(\u0026#34;DOMContentLoaded\u0026#34;, () =\u0026gt; { let searchResults = []; const searchWrapper = document.querySelector(\u0026#34;aside[role=search]\u0026#34;); const searchResultElement = searchWrapper.querySelector(\u0026#34;.search-results\u0026#34;); const searchInput = searchWrapper.querySelector(\u0026#34;input\u0026#34;); const toggleSearch = (searchWrapper, searchInput) =\u0026gt;{ if (searchWrapper.classList.contains(\u0026#34;active\u0026#34;)) { searchWrapper.classList.add(\u0026#34;visible\u0026#34;); setTimeout(() =\u0026gt; { searchWrapper.classList.remove(\u0026#34;visible\u0026#34;); }, 300); searchWrapper.classList.remove(\u0026#34;active\u0026#34;); } else { searchWrapper.classList.add(\u0026#34;active\u0026#34;); searchInput.focus(); } } document.querySelectorAll(\u0026#34;.toggle-search\u0026#34;).forEach(el =\u0026gt; { el.addEventListener(\u0026#34;click\u0026#34;, e =\u0026gt; { toggleSearch(searchWrapper, searchInput); }); }); window.addEventListener(\u0026#34;keydown\u0026#34;, e =\u0026gt; { // dismiss search on ESC if (e.key == \u0026#34;Escape\u0026#34; \u0026amp;\u0026amp; searchWrapper.classList.contains(\u0026#34;active\u0026#34;)) { e.preventDefault(); toggleSearch(searchWrapper, searchInput); } // open search on CTRL+SHIFT+F if (e.ctrlKey \u0026amp;\u0026amp; e.shiftKey \u0026amp;\u0026amp; e.key == \u0026#34;F\u0026#34; \u0026amp;\u0026amp; !searchWrapper.classList.contains(\u0026#34;active\u0026#34;)) { e.preventDefault(); toggleSearch(searchWrapper, searchInput); } }); const tags = (tags, searchString) =\u0026gt; { let tagHTML = (tags.split(\u0026#34; ; \u0026#34;) || []) .filter(i =\u0026gt; { return i \u0026amp;\u0026amp; i.length \u0026gt; 0; }) .map(i =\u0026gt; { return \u0026#34;\u0026lt;span class=\u0026#39;tag\u0026#39;\u0026gt;\u0026#34; + mark(i, searchString) + \u0026#34;\u0026lt;/span\u0026gt;\u0026#34;; }) return tagHTML.join(\u0026#34;\u0026#34;); } const mark = (content, search) =\u0026gt; { if (search) { let pattern = /^[a-zA-Z0-9]*:/i; search.split(\u0026#34; \u0026#34;).forEach(s =\u0026gt; { if (pattern.test(s)) { s = s.replace(pattern, \u0026#34;\u0026#34;); } if (s \u0026amp;\u0026amp; s.startsWith(\u0026#34;+\u0026#34;)) { s = s.substring(1); } if (s \u0026amp;\u0026amp; s.indexOf(\u0026#34;~\u0026#34;) \u0026gt; 0 \u0026amp;\u0026amp; s.length \u0026gt; s.indexOf(\u0026#34;~\u0026#34;) \u0026amp;\u0026amp; parseInt(s.substring(s.indexOf(\u0026#34;~\u0026#34;) + 1)) == s.substring(s.indexOf(\u0026#34;~\u0026#34;) + 1) ) { s = s.substring(0, s.indexOf(\u0026#34;~\u0026#34;)); } if (!s || s.startsWith(\u0026#34;-\u0026#34;)) { return; } let re = new RegExp(s, \u0026#34;i\u0026#34;); content = content.replace(re, m =\u0026gt; { return \u0026#34;\u0026lt;mark\u0026gt;\u0026#34;+m+\u0026#34;\u0026lt;/mark\u0026gt;\u0026#34;; }); }); } return content; } fetch(\u0026#34;/search\u0026#34;) .then(response =\u0026gt; response.json()) .then(result =\u0026gt; { const searchContent = result; const searchIndex = lunr(builder =\u0026gt; { builder.ref(\u0026#34;id\u0026#34;) builder.field(\u0026#34;content\u0026#34;); builder.field(\u0026#34;tag\u0026#34;); builder.field(\u0026#34;title\u0026#34;); builder.field(\u0026#34;url\u0026#34;); builder.field(\u0026#34;type\u0026#34;); Array.from(result).forEach(doc =\u0026gt; { builder.add(doc) }, builder) }) searchInput.removeAttribute(\u0026#34;disabled\u0026#34;); searchInput.addEventListener(\u0026#34;keyup\u0026#34;, e =\u0026gt; { let searchString = e.target.value; if (searchString \u0026amp;\u0026amp; searchString.length \u0026gt; 2) { try { searchResults = searchIndex.search(searchString); } catch (err) { if (err instanceof lunr.QueryParseError) { return; } } } else { searchResults = []; } if (searchResults.length \u0026gt; 0) { searchResultElement.innerHTML = searchResults.map(match =\u0026gt; { let item = searchContent.find(el =\u0026gt; { return el.id == parseInt(match.ref); }); return \u0026#34;\u0026lt;li\u0026gt;\u0026#34; + \u0026#34;\u0026lt;h4 title=\u0026#39;field: title\u0026#39;\u0026gt;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39;\u0026gt;\u0026#34; + mark(item.title, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026lt;/h4\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;type\u0026#39;\u0026gt;\u0026#34; + item.type + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;summary\u0026#39; title=\u0026#39;field: content\u0026#39;\u0026gt;\u0026#34; + mark((item.content.length \u0026gt; 200 ? (item.content.substring(0, 200) + \u0026#34;...\u0026#34;) : item.content), searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;tags\u0026#39; title=\u0026#39;field: tag\u0026#39;\u0026gt;\u0026#34; + tags(item.tag, searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39; title=\u0026#39;field: url\u0026#39;\u0026gt;\u0026#34; + mark(item.url, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026#34; + \u0026#34;\u0026lt;/li\u0026gt;\u0026#34;; }).join(\u0026#34;\u0026#34;); } else { searchResultElement.innerHTML = \u0026#34;\u0026lt;li\u0026gt;\u0026lt;p class=\u0026#39;no-result\u0026#39;\u0026gt;No results found\u0026lt;/p\u0026gt;\u0026lt;/li\u0026gt;\u0026#34;; } }); }) .catch(err =\u0026gt; { console.error(err); }); }); There are some functions in there to make a nice transition for open/closing the search. But the important part is\nfetch(\u0026#34;/search\u0026#34;) .then(response =\u0026gt; response.json()) .then(result =\u0026gt; { const searchContent = result; const searchIndex = lunr(builder =\u0026gt; { builder.ref(\u0026#34;id\u0026#34;) builder.field(\u0026#34;content\u0026#34;); builder.field(\u0026#34;tag\u0026#34;); builder.field(\u0026#34;title\u0026#34;); builder.field(\u0026#34;url\u0026#34;); builder.field(\u0026#34;type\u0026#34;); Array.from(result).forEach(doc =\u0026gt; { builder.add(doc) }, builder) }) searchInput.removeAttribute(\u0026#34;disabled\u0026#34;); searchInput.addEventListener(\u0026#34;keyup\u0026#34;, e =\u0026gt; { let searchString = e.target.value; if (searchString \u0026amp;\u0026amp; searchString.length \u0026gt; 2) { try { searchResults = searchIndex.search(searchString); } catch (err) { if (err instanceof lunr.QueryParseError) { return; } } } else { searchResults = []; } if (searchResults.length \u0026gt; 0) { searchResultElement.innerHTML = searchResults.map(match =\u0026gt; { let item = searchContent.find(el =\u0026gt; { return el.id == parseInt(match.ref); }); return \u0026#34;\u0026lt;li\u0026gt;\u0026#34; + \u0026#34;\u0026lt;h4 title=\u0026#39;field: title\u0026#39;\u0026gt;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39;\u0026gt;\u0026#34; + mark(item.title, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026lt;/h4\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;type\u0026#39;\u0026gt;\u0026#34; + item.type + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;summary\u0026#39; title=\u0026#39;field: content\u0026#39;\u0026gt;\u0026#34; + mark((item.content.length \u0026gt; 200 ? (item.content.substring(0, 200) + \u0026#34;...\u0026#34;) : item.content), searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;tags\u0026#39; title=\u0026#39;field: tag\u0026#39;\u0026gt;\u0026#34; + tags(item.tag, searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39; title=\u0026#39;field: url\u0026#39;\u0026gt;\u0026#34; + mark(item.url, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026#34; + \u0026#34;\u0026lt;/li\u0026gt;\u0026#34;; }).join(\u0026#34;\u0026#34;); } else { searchResultElement.innerHTML = \u0026#34;\u0026lt;li\u0026gt;\u0026lt;p class=\u0026#39;no-result\u0026#39;\u0026gt;No results found\u0026lt;/p\u0026gt;\u0026lt;/li\u0026gt;\u0026#34;; } }); }) .catch(err =\u0026gt; { console.error(err); }); This loads the json in a Promise from our search url. Then when that is successful it will load the data into lunr.\nconst searchContent = result.data; const searchIndex = lunr(function () { this.ref(\u0026#34;id\u0026#34;) this.field(\u0026#34;content\u0026#34;); this.field(\u0026#34;tag\u0026#34;); this.field(\u0026#34;title\u0026#34;); this.field(\u0026#34;url\u0026#34;); Array.from(result.data).forEach(function (doc) { this.add(doc) }, this) }) So now lunr indexes all the fields we wanted. Here the index from before is used as a reference\nthis.ref(\u0026#34;id\u0026#34;) and what other fields to index\nthis.field(\u0026#34;content\u0026#34;); this.field(\u0026#34;tag\u0026#34;); this.field(\u0026#34;title\u0026#34;); this.field(\u0026#34;url\u0026#34;); And then finally, load the results into a template, that can be injected into the ul.search-results element\nif (searchResults.length \u0026gt; 0) { searchResultElement.innerHTML = searchResults.map(function (match) { let item = searchContent.find(function(e) { return e.id == parseInt(match.ref); }); return \u0026#34;\u0026lt;li\u0026gt;\u0026#34; + \u0026#34;\u0026lt;h4 title=\u0026#39;field: title\u0026#39;\u0026gt;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39;\u0026gt;\u0026#34; + mark(item.title, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026lt;/h4\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;summary\u0026#39; title=\u0026#39;field: content\u0026#39;\u0026gt;\u0026#34; + mark((item.content.length \u0026gt; 200 ? (item.content.substring(0, 200) + \u0026#34;...\u0026#34;) : item.content), searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;p class=\u0026#39;tags\u0026#39; title=\u0026#39;field: tag\u0026#39;\u0026gt;\u0026#34; + tags(item.tag, searchString) + \u0026#34;\u0026lt;/p\u0026gt;\u0026#34; + \u0026#34;\u0026lt;a href=\u0026#39;\u0026#34; + item.url + \u0026#34;\u0026#39; title=\u0026#39;field: url\u0026#39;\u0026gt;\u0026#34; + mark(item.url, searchString) + \u0026#34;\u0026lt;/a\u0026gt;\u0026#34; + \u0026#34;\u0026lt;/li\u0026gt;\u0026#34;; }).join(\u0026#34;\u0026#34;); } else { searchResultElement.innerHTML = \u0026#34;\u0026lt;li\u0026gt;\u0026lt;p class=\u0026#39;no-result\u0026#39;\u0026gt;No results found\u0026lt;/p\u0026gt;\u0026lt;/li\u0026gt;\u0026#34;; } And all wrapped nicely into the keypup event on the search input.\nBOOM, all set . Happy copy-pasting\nIf you want to use Fusejs, there\u0026rsquo;s a nice post here\n","id":4,"tag":"development ; lang-en ; hugo ; lunrjs ; search","title":"Add Search Functionality to Your Hugo Static Site","type":"post","url":"https://rikdegroot.io/-/2019/09/03/add-search-functionality-to-your-hugo-static-site/"},{"content":"Recently I found a blog post I never really published (on an ancient blogger account). However I am not very active in C#.NET nowadays, so maybe no-one is using this anymore.\nAnyway this might actually help some people, so here it is, dug up from the past\u0026hellip;\now, btw the source is gone\u0026hellip; ¯\\_(ツ)_/¯\nUIAutomation of TreeViewAdv I am using the open source tool TreeViewAdv form Aga.Controls.\nAs a QA\u0026rsquo;er I tried to automate this control, but unfortunately the control was not exposing any elements inside the tree. It turned out that UIAutomation was not implemented.\nThis was an enormous drawback for me, because it narrowed down to a lot of tests with mouseclicks at (x,y) coordinates, which made the tests less robust, and even fail at different screen resolutions.\nI started Googling for solutions, but I did not find many. As a result I started looking into how-to\u0026rsquo;s for custom winforms controls. Finally I came across this blog post which helped me implementing methods to expose the elements in the tree to the UIAutomation framework.\nMichael Bernstein wrote a blog which also goes in depth on this topic. However the sources are not available anymore.\nI ended up forking the control, and implementing UIAutomation detection for the treeview. Because I had quite a hard time figuring out how to implement it all, I will try to explain how I did it, and explain all the steps I did to come to the result.\nThe forked project can be found at https://github.com/hwdegroot/treeviewadv. In the project you will find the TreeViewAdv with UIAutomation see README and an extension to TestStack.White which helps white to detect the element as a treeview/treenode element and the exposed patterns (in the example the ExpandCollapsePattern).\nThis can be extended to any other pattern that is supported by the UIAutomation framework.\nI hope this post will help you implementing and understanding UIAtomationPatterns and providers and giving a handson Automation implementation at the same time.\nFor any questions feel free to ask.\nImplementing UIAutomation Serverside Step 1. TreeView Step 1.1 Implement a Automation provider for the TreeView For the implementation of the provider, the hierarchy is as described in the blogpost of Guy Barker. This implements the following three base classes:\n BaseFragmentProvider BaseFragmentRootProvider BaseSimpleProvider You will find them in: Aga.Controls.UIAutomation besides the Aga.Controls project. For the result however, this is not mandatory.\nIn this example the code snippets refer to those classes.\nSecondly there are implementation for the Providers in the treeview project. These providers will be called by the Automation framework, and will expose the properties and methods needed. The code snippet below shows the relevant implementation of the TreeViewAdv provider.\nnamespace Aga.Controls.Providers { public class TreeViewAdvProvider : BaseFragmentRootProvider, IExpandCollapseProvider { ... /// \u0026lt;summary\u0026gt; /// Gets the window handle /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;handle\u0026lt;/returns\u0026gt; protected override IntPtr GetWindowHandle() { return _treeViewAdv.Handle; } /// \u0026lt;summary\u0026gt; /// Gets the name of the Fragmentroot /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;Name of treeview\u0026lt;/returns\u0026gt; protected override string GetName() { return Name; } /// \u0026lt;summary\u0026gt; /// Get the bounding rect by consulting the control. /// \u0026lt;/summary\u0026gt; public override Rect BoundingRectangle { get { var screenRect = _treeViewAdv.RectangleToScreen(_treeViewAdv.DisplayRectangle); return new Rect(screenRect.Left, screenRect.Top, screenRect.Width, screenRect.Height); } } /// \u0026lt;summary\u0026gt; /// Return first child. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;First direct child\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetFirstChild() { ... } /// \u0026lt;summary\u0026gt; /// Returns last child. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;last child node\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetLastChild() { ... } /// \u0026lt;summary\u0026gt; /// Return next nodeat the same level of this node. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;Nest sibling node\u0026lt;/returns\u0026gt; public override IRawElementProviderFragment ElementProviderFromPoint(double x, double y) { if (_treeViewAdv == null) return null; var clientPoint = _treeViewAdv.PointToClient(new Point((int)x, (int)y)); var node = _treeViewAdv.GetNodeAt(clientPoint); // First check if the clicked point is on a node in the treeview if (node != null) { return new TreeNodeAdvProvider(_treeViewAdv, this, node); } // if it is not a node, it might be the column header var column = _treeViewAdv.GetColumnAt(clientPoint); if (column != null) { return new TreeColumnProvider(_treeViewAdv, this, column); } return this; } ... /// \u0026lt;summary\u0026gt; /// Gets Pattern provider from \u0026lt;paramref name=\u0026#34;patternId\u0026#34; /\u0026gt; /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;patternId\u0026#34;\u0026gt;PatternId\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;self\u0026lt;/returns\u0026gt; public override object GetPatternProvider(int patternId) { switch (patternId) { case UIAConstants.UIA_EXPAND_COLLAPSE_PATTERN_ID: return this; default: return base.GetPatternProvider(patternId); } } } } Important here is the override of\npublic override object GetPatternProvider(int patternId) { ... } This method will tell the automation framework what Providers are implemented. Later this will help with getting the ExpandCollapsePattern from the AutomationElement\nWhen I want to access the Collapse() and Expand() method through UIAutomation, the automation framework will look for exposed patterns. This method will teel which providers are supported by my custom provider. The switch statement tells which providers are supported. These are identified by integers (patternId), which in are in a constant class UIAConstants.\nStep 1.2 Capture the WndProc in the custom control Afterwards The provider is added to the control, via a few lines of codes, which tell UIAutomation to return the provider when the element is selected in the tree.\nnamespace Aga.Controls.Tree { /// \u0026lt;summary\u0026gt; /// Extensible advanced \u0026lt;see cref=\u0026#34;TreeView\u0026#34;/\u0026gt; implemented in 100% managed C# code. /// Features: Model/View architecture. Multiple column per node. Ability to select /// multiple tree nodes. Different types of controls for each node column: /// \u0026lt;see cref=\u0026#34;CheckBox\u0026#34;/\u0026gt;, Icon, Label... Drag and Drop highlighting. Load on /// demand of nodes. Incremental search of nodes. /// \u0026lt;/summary\u0026gt; public partial class TreeViewAdv : Control { ... #region UIAutomation detection private TreeViewAdvProvider _provider; protected override void WndProc(ref Message m) { // Handle WM_GETOBJECT. Without this, UIA doesn\u0026#39;t // know the chart has a UIA Provider implementation. if (m.Msg == 0x3D /* WM_GETOBJECT */) { m.Result = NativeMethods.UiaReturnRawElementProvider( m.HWnd, m.WParam, m.LParam, this.Provider ); } else { base.WndProc(ref m); } } public TreeViewAdvProvider Provider { get { if (_provider == null) { _provider = new TreeViewAdvProvidger(this); } return _provider; } } #endregion } } Which element is clicked, is defined by\npublic override IRawElementProviderFragment ElementProviderFromPoint(double x, double y) { var clientPoint = _treeViewAdv.PointToClient(new Point((int)x, (int)y)); var node = _treeViewAdv.GetNodeAt(clientPoint); var column = _treeViewAdv.GetColumnAt(clientPoint); if (node != null) { return new TreeNodeAdvProvider(_treeViewAdv, this, node); } if (column != null) { return new TreeColumnProvider(_treeViewAdv, this, column.Index); } return _treeViewAdv == null ? null : new TreeViewAdvProvider(_treeViewAdv); } in the TreeViewAdvProvider.\nSo now the TreeView will return a the relevant provider when a MouseClick is recorded in the control. Next all that needs to be done is repeat the same step for the headers and the nodes, and let the treeview decide which provider should be returned, depending on whether a node or a column is clicked. If neither is present at the clicked point, it will return the provider of the tree itself.\nStep 2. TreeNode The same implementation can be repeated for the TreeNodeAdv. The code snippet below shows this implementation.\nnamespace Aga.Controls.Providers { public class TreeNodeAdvProvider : BaseFragmentProvider, IExpandCollapseProvider { ... /// \u0026lt;summary\u0026gt; /// Gets Pattern provider from \u0026lt;paramref name=\u0026#34;patternId\u0026#34; /\u0026gt; /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;patternId\u0026#34;\u0026gt;PatternId\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;self\u0026lt;/returns\u0026gt; public override object GetPatternProvider(int patternId) { switch (patternId) { case UIAConstants.UIA_EXPAND_COLLAPSE_PATTERN_ID: return this; default: return base.GetPatternProvider(patternId); } } ... /// \u0026lt;summary\u0026gt; /// Get the bounding rect by consulting the control. /// \u0026lt;/summary\u0026gt; public override Rect BoundingRectangle { get { var screenRect = _treeViewAdv.RectangleToScreen(_treeViewAdv.GetNodeBounds(_node)); AddStaticProperty(UIAConstants.UIA_IS_OFFSCREEN_PROPERTY_ID, !_node.IsVisible); if (!_node.IsVisible) return Rect.Empty; var margin = _treeViewAdv.Margin; double offsetTop = (double) ( _treeViewAdv.ColumnHeaderHeight - ( _treeViewAdv._vScrollBar.Value * _treeViewAdv.RowHeight ) ); var offsetLeft = (margin.Left / 2) - _treeViewAdv._hScrollBar.Value; return new Rect( screenRect.Left + offsetLeft, screenRect.Top + offsetTop, screenRect.Width, screenRect.Height ); } } /// \u0026lt;summary\u0026gt; /// Return first child. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;First direct child\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetFirstChild() { ... } /// \u0026lt;summary\u0026gt; /// Returns last child. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;last child node\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetLastChild() { ... } /// \u0026lt;summary\u0026gt; /// Return next nodeat the same level of this node. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;Nest sibling node\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetNextSibling() { ... } /// \u0026lt;summary\u0026gt; /// Gets the preceeding node at the same level. /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;Preceeding node.\u0026lt;/returns\u0026gt; protected override IRawElementProviderFragment GetPreviousSibling() { ... } ... } } This provider exposes another pattern\nAddStaticProperty(UIAConstants.UIA_IS_OFFSCREEN_PROPERTY_ID, !_node.IsVisible); This property tells the Automation framework if the AutomationElement is offscreen. Offscreen elements are not clickable by the automationframwork (see MSDN, which is expected behaviour in this case.)\nThe snippet below shows how this is used to make invisible nodes inaccesible to the Automation Client\npublic override Rect BoundingRectangle { get { ... AddStaticProperty(UIAConstants.UIA_IS_OFFSCREEN_PROPERTY_ID, !_node.IsVisible); ... } } Step 3. TreeHeader and columns This example will distinguish between the TreeHeader and the Column header, but that is not mandatory.\nMainly one would want to be able to identify the tree columns because the can be used for sorting.\nThe implementation is pretty straight-forward, compared to the previous two providers.\nStep 4. ExpandCollapseProvider Next the methods and properties of IExpandCollapseProvider have to be implemented to tell the control what to do when called from the AutomationElement.\nThe snippet below shows the implementation off the TreeNodeAdvProvider. For the TreeViewAdvProvider this is similar.\n/// \u0026lt;summary\u0026gt; /// Exposes Expand from node. /// \u0026lt;/summary\u0026gt; public void Expand() { if (_node.CanExpand \u0026amp;\u0026amp; !_node.IsExpanded) _node.Expand(); } /// \u0026lt;summary\u0026gt; /// Exposes Collapse from node. /// \u0026lt;/summary\u0026gt; public void Collapse() { if (_node.CanExpand \u0026amp;\u0026amp; _node.IsExpanded) _node.Collapse(); } ExpandCollapseState IExpandCollapseProvider.ExpandCollapseState { get { return ExpandCollapseState; } } /// \u0026lt;summary\u0026gt; /// Custom getter. /// Gets the state, expanded or collapsed, of the control. /// \u0026lt;/summary\u0026gt; public ExpandCollapseState ExpandCollapseState { get { if (_node.IsLeaf) return ExpandCollapseState.LeafNode; if (_node.IsExpanded) return ExpandCollapseState.Expanded; if (_node.IsExpandedOnce) return ExpandCollapseState.PartiallyExpanded; return ExpandCollapseState.Collapsed; } } Step 5. Exposing ExpandCollapsePatternProvider We want the Automation framework to call our custoim implementation when the ExpandCollapsePattern is called. Therefore we need to tell the provider to return our provider instead of the base provider for the exposed methods. This is implemented through the GetPatternProvider(). The code snippet below shows that when the AutomationELement is called with the ExpandCollapsePatternId, our custom provider is returned, and not the base provider.\n/// \u0026lt;summary\u0026gt; /// Gets Pattern provider from \u0026lt;paramref name=\u0026#34;patternId\u0026#34; /\u0026gt; /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;patternId\u0026#34;\u0026gt;PatternId\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;self\u0026lt;/returns\u0026gt; public override object GetPatternProvider(int patternId) { switch (patternId) { case UIAConstants.UIA_EXPAND_COLLAPSE_PATTERN_ID: return this; default: return base.GetPatternProvider(patternId); } } Note the UIAConstants.UIA_EXPAND_COLLAPSE_PATTERN_ID which will do that magic for us.\nExtending White Framework with custom UIItem ClientSide When taking all the effort to copy-paste this code, it would be nice if it also works. Below is a snippet of an extension method for the Teststack.White framework, which will define a TreeViewItem and a TreeNodeItem that will implement our custom control, and will also call the Expand() and Collapse() methods.\nStep 6. Extend White with the custom UIItem First we need an extension to white. Since they both have implement the IExpandCollaseProvider the inherit from the base class ExpandCollapseUIItem.\nThe snippet below tries to parse the pattern to the ExpandCollapsePattern\nprotected ExpandCollapsePattern GetExpandCollapsePattern(AutomationElement targetControl) { ExpandCollapsePattern expandCollapsePattern; try { expandCollapsePattern = targetControl.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern; } catch (InvalidOperationException) { return null; } return expandCollapsePattern; } Here it becomes clear why we had to tell our provider to return our own control for the UIAConstants.UIA_EXPAND_COLLAPSE_PATTERN_ID, because the AutomationElement.GetCurrentPattern(ExpandCollapsePattern.Pattern) will be null if we did not take that effort.\nNext Expand() and Collapse() can be called as method of the pattern (the snippet shows Expand()).\n/// \u0026lt;summary\u0026gt; /// Implements Collapse /// \u0026lt;/summary\u0026gt; public virtual void Expand() { var expandCollapsePattern = GetExpandCollapsePattern(automationElement); if ( expandCollapsePattern == null || expandCollapsePattern.Current.ExpandCollapseState == ExpandCollapseState.LeafNode ) { return; } try { if ( expandCollapsePattern.Current.ExpandCollapseState == ExpandCollapseState.Collapsed || expandCollapsePattern.Current.ExpandCollapseState == ExpandCollapseState.PartiallyExpanded ) { expandCollapsePattern.Expand(); } } catch (ElementNotEnabledException enee) { throw; } catch (InvalidOperationException ioe) { throw; } } Now the extension for the white framework is a piece of cake:\nTreeNodeItem namespace TestStack.White.UIITems.Custom { [ControlTypeMapping(CustomUIItemType.Custom)] public class TreeNodeItem : ExpandCollapseUIItem { public TreeNodeItem(AutomationElement automationElement, ActionListener actionListener) : base(automationElement, actionListener) { } protected TreeNodeItem() { } } } TreeViewItem namespace TestStack.White.UIITems.Custom { [ControlTypeMapping(CustomUIItemType.Custom)] public class TreeViewItem : ExpandCollapseUIItem { public TreeViewItem(AutomationElement automationElement, ActionListener actionListener) : base(automationElement, actionListener) { } protected TreeViewItem() { } public override void Expand() { var expandCollapsePattern = GetExpandCollapsePattern(automationElement); if ( expandCollapsePattern == null || expandCollapsePattern.Current.ExpandCollapseState == ExpandCollapseState.LeafNode ) { return; } try { expandCollapsePattern.Expand(); } catch (ElementNotEnabledException enee) { throw; } catch (InvalidOperationException) { throw; } } public override void Collapse() { var expandCollapsePattern = GetExpandCollapsePattern(automationElement); if ( expandCollapsePattern == null || expandCollapsePattern.Current.ExpandCollapseState == ExpandCollapseState.LeafNode ) { return; } try { expandCollapsePattern.Collapse(); } catch (ElementNotEnabledException enee) { throw; } catch (InvalidOperationException) { throw; } } } } Using TestStack.White to test the TreeView Test TreeView and the Expand and Collapse Behaviour.\nAfter Test initialization this yields to te following test\n[TestMethod] public void select_treeview() { using ( CoreAppXmlConfiguration.Instance.ApplyTemporarySetting(c =\u0026gt; { c.FindWindowTimeout = 1000; c.BusyTimeout = 1500; }) ) { var searchCriteria = SearchCriteria.ByAutomationId(\u0026#34;MainForm\u0026#34;); var mainForm = _application.GetWindow(searchCriteria, InitializeOption.NoCache); mainForm.Focus(); var tree = mainForm.Get\u0026lt;TreeViewItem\u0026gt;(SearchCriteria.ByAutomationId(\u0026#34;treeViewAdv1\u0026#34;)); tree.Expand(); var node1 = tree.Get\u0026lt;TreeNodeItem\u0026gt;(SearchCriteria.ByAutomationId(\u0026#34;Root1\u0026#34;)); var node14 = node1.Get\u0026lt;TreeNodeItem\u0026gt;(SearchCriteria.ByAutomationId(\u0026#34;Root1.Child4\u0026#34;)); var node143 = node14.Get\u0026lt;TreeNodeItem\u0026gt;(SearchCriteria.ByAutomationId(\u0026#34;Root1.Child4.Child43\u0026#34;)); Assert.IsTrue(node143.Visible); Assert.IsTrue(node14.Visible); node14.Collapse(); node143 = node14.Get\u0026lt;TreeNodeItem\u0026gt;(SearchCriteria.ByAutomationId(\u0026#34;Root1.Child4.Child43\u0026#34;)); Assert.IsFalse(node143.Visible); tree.Collapse(); Assert.IsFalse(node14.Visible); } } Here you can see that the custom control can be used to get a TreeNodeItem and a TreeViewItem. ChildNodes are visible when the parent is Expanded, but not when the parent is collapsed. The visibility is exposed through the IsOffScreenProperty. This is achieved by toggling the UIAConstants.UIA_IS_OFFSCREEN_PROPERTY_ID in the provider, based on the node\u0026rsquo;s visibility.\nHope this helps anyone with a full-circle implementation.\n","id":5,"tag":"development ; lang-en ; TreeViewAdv ; C# ; .Net ; TestStack-White","title":"UI automation of a Custom Control","type":"post","url":"https://rikdegroot.io/-/2019/08/02/ui-automation-of-a-custom-control/"},{"content":"Not so long ago I had to make a static website, and was figuring out how to do this. I came across hugo. And I really liked how quickly you can build and publish a website from markdown.\nA while ago google made the .dev available for peanuts, so, impulsively, I bought one. From $12 you can get one, and start doing awesome stuff with this. But I didn\u0026rsquo;t. I parked the domain for a while. There are plenty of cloud providers that will host your website for you, like AWS, Netlify and many more.\nBut I had no clue where to host this, because I am lazy and cheap. Then for work I had to do something similar, and I wondered if I could just host my GitLab Pages on my own domain. And guess what, you can, and it is amazingly simple!\nI started to write out some boilerplate (easy), picked a theme I liked (easy, I used m10c), and off I went.\nHere I will try to explain how I got from writing some basic markdown to running it on my own domain (well, technically it doesn\u0026rsquo;t. But it appears so).\nYou can find the repository of this site here. I like yaml over toml. Therefor I will use the yaml configuration option from hugo.\nMy config is as follows, but also available here\ntitle:RikdeGrootbaseURL:https://www.forsure.dev/enableRobotsTXT:truelanguageCode:en-usassetsDir:content/assetsthemesDir:themesmetaDataFormat:yamlpermalinks:posts:/-/:year/:month/:day/:titletags:/:slugpaginate:10theme:m10cenableGitInfo:truegoogleAnalytics:\u0026lt;GAtrackingcode\u0026gt; # enable auto code highlighting without effortpygmentsCodefencesGuessSyntax:truepygmentsUseClasses:truepygmentsStyle:monokaipygmentsCodeFences:true# Site parametersparams:author:RikdeGrootdescription:Code,cookandbake.TechLead@FinLeaseNLavatar:assets/images/rik-de-groot.jpgimages:- assets/og/cover.pngsocial:- name:gitlaburl:https://gitlab.com/hwdegroot- name:twitterurl:https://twitter.com/hwdegroot- name:linkedinurl:https://www.linkedin.com/in/rikhwdegroot/## Use the green stylesstyle:darkestColor:\u0026#34;#282e37\u0026#34;darkColor:\u0026#34;#3d434c\u0026#34;primaryColor:\u0026#34;#67eba2\u0026#34;lightColor:\u0026#34;#d3d3d3\u0026#34;lighestColor:\u0026#34;#fff\u0026#34;The nice thing about using the tools that are popular, is that a lot of people already figured out stuff for you. All you need to do is Google it. So I did. You can find all about deploying a hugo app to GitLab pages in this example.\nI used the following .gitlab-ci.yml configuration to get the job done. Gitlab published a docker container, that can be used to build your project (see the above example form GitLab). But I like to have the extended features as well, so I created my own which is available in the container registry of the project. But not too much credits to myself, because I based it on the container from jguyomard. You can find the project on GitHub.\n# .gitlab-ci.ymlstages:- pages# the theme is submoduled, so make sure to do a recursive clonevariables:GIT_SUBMODULE_STRATEGY:recursivepages:stage:pagesimage:registry.gitlab.com/hwdegroot/forsure.dev/hugo:latestbefore_script:- gitclean-ffdxscript:# files are in a subdirectory of the project- cdsite- hugo--contentDircontent/--configconfig/config.yaml--destination../public/artifacts:paths:- publiconly:- tags- mainNow that you build your static site in GitLab, and deploy it to GitLab Pages, you can add your own domain to GitLab pages by following a few steps.\nAdd your domain to GitLab Pages Now we can add the domain to GitLab Pages. In your project, go to Settings \u0026gt; Pages. Here click the New domain button.\nFill in your domain, and set the Automatic certificate management using Let's Encrypt switch on.\n Configure your own domain in GitLab Pages settings. \u0026times; Verify your domain To verify your domain, you will need to add two fields to your DNS. I am using Google domains, but it is all more or less the same. All you need is privileges to alter the DNS settings of your domain.\nYoul will have to add two records, so GitLab can verify you own the domain.\nFirst you will have to add a CNAME record \u0026lt;www.yourdomain.dev\u0026gt; CNAME \u0026lt;yourusername\u0026gt;.gitlab.io. to forward the url to gitlab pages, and a TXT record to verify the domain is yours _gitlab-pages-verification-code.www.yourdomain.dev TXT gitlab-pages-verification-code=\u0026lt;somerandomcode\u0026gt;.\n Configure dns records in google domains. \u0026times; Then hit the verify button. It might not work straight away because the records need to be synced. But in my case it was less than 5 minutes.\n Before adding DNS records, domain fails to verify \u0026times; Verify that the domain is yours after adding the verification code to your DNS \u0026times; If it worked, the status will change to verified\n Domain verified by GitLab. \u0026times; And in the overview, you will see that your domain is listed in the Access pages section\n Domain added to GitLab Pages. \u0026times; Add a let\u0026rsquo;s encrypt certificate to your page Gitlab has this great help on how to use let\u0026rsquo;s encrypt in combination with GitLab Pages. Following the steps from their manual will get you all you need to register your https certificate to your domain.\nTL;DR;\nI had to register using certbot. Just install the binary using the package manager of your OS.\ncertbot certonly -a manual -d www.forsure.dev --email hwdegroot@gmail.com And it gave me the code in return of format A.B. Mine looked like\nMXDCs3RTdKM5KNIJFbogSwTLoiduCbtyHKZ2k1zxvWQ.JEZ1UZmU4x3wCgSiV9gom4Irb8AgSkVFrsCju6sFLa8 then create a directory public/.well-known/acme-challenge/A, in my case public/.well-known/acme-challenge/MXDCs3RTdKM5KNIJFbogSwTLoiduCbtyHKZ2k1zxvWQ/. Inside this directory, put a file index.html with the full A.B key in it, just as suggested by the certbot output\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NOTE: The IP of this machine will be publicly logged as having requested this certificate. If you\u0026#39;re running certbot in manual mode on a machine that is not your server, please ensure you\u0026#39;re okay with that. Are you OK with your IP being logged? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (Y)es/(N)o: y - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file containing just this data: MXDCs3RTdKM5KNIJFbogSwTLoiduCbtyHKZ2k1zxvWQ.JEZ1UZmU4x3wCgSiV9gom4Irb8AgSkVFrsCju6sFLa8 And make it available on your web server at this URL: http://www.forsure.dev/.well-known/acme-challenge/TfBYIcpd4uO4y4kkH3GAYdsUXeCycAYLPcsF6JEiq1I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - At this point, pause. DO NOT continue yet. If you did, no problem. Validation will fail, but the next time you run it, you will get the same code.\nSo the file public/.well-known/acme-challenge/MXDCs3RTdKM5KNIJFbogSwTLoiduCbtyHKZ2k1zxvWQ/index.html would look like\n\u0026lt;!-- index.html --\u0026gt; MXDCs3RTdKM5KNIJFbogSwTLoiduCbtyHKZ2k1zxvWQ.JEZ1UZmU4x3wCgSiV9gom4Irb8AgSkVFrsCju6sFLa8 Basically that is it. You can continue the certificate registration, although it will probably fail. Make sure that GitLab deploys your pages with the certificate directory .well-known/acme-challenge/A in it. You can check this by visiting https://\u0026lt;your-domain\u0026gt;/.wel-known/acme-challenge/A/. It should serve you this html page with the key in it. Then you can run the certbot command again, and it should present you the success message\nIMPORTANT NOTES - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/blog.rikdegroot.dev/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/blog.rikdegroot.dev/privkey.pem Your cert will expire on 2019-10-17. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run \u0026#34;certbot renew\u0026#34; - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let\u0026#39;s Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le You\u0026rsquo;re done!\nHope it helps\n","id":6,"tag":"development ; gitlab ; google domains ; let's encrypt ; lang-en","title":"How to run and deploy gitlab pages on your own domain","type":"post","url":"https://rikdegroot.io/-/2019/07/18/how-to-run-and-deploy-gitlab-pages-on-your-own-domain/"},{"content":"UPDATE: This has expired, however if you want to use our service, got to https://easee.online, or feel free to reach out.\nAt @easeeonline we want everybody to have the oportunity to see clearly. For this reason we are giving out vouchers to get a free validation. Visit us at easee.online or do the test and get your results validated by one of our optometrists.\nsee clearly - in 80 days. One in every three people does not have the right vision correction.\nHelp us solve this problem in just 80 days, by giving everyone easy access to eye testing. Worldwide.\nAll you need to do: Participate in Easee’s online eye-exam and share the free voucher code\nQ3-RED-R ","id":7,"tag":"eyesight ; see clearly ; easee ; lang-en","title":"See clearly - in 80 days","type":"post","url":"https://rikdegroot.io/-/2019/07/17/see-clearly-in-80-days/"}]