From fb342ef7c3dc1e759e0039cbc45bf83b01a33762 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:15:14 +0100 Subject: [PATCH 01/32] Add missing fields to LICENSE --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index b62a9b5..46294d7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -179,7 +179,7 @@ recommend that a file or class name and description of purpose be included on the same “printed page” as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Data Science & Advanced Analytics, NHS BSA Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 6e70e8ec65e121bf1b8a0ec933e237925ec8db95 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:16:51 +0100 Subject: [PATCH 02/32] Add new markdown pages + modified namespaces in modules --- R/app_server.R | 5 ++--- R/app_ui.R | 12 ++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/R/app_server.R b/R/app_server.R index 1c429f7..07562f4 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -6,7 +6,6 @@ #' @noRd app_server <- function(input, output, session) { # Your application server logic - mod_markdown_example_server("markdown_example_ui_1") - mod_chart_example_server("chart_example_ui_1") - mod_scrollytell_example_server("scrollytell_example_1") + mod_chart_example_server("chart_example") + mod_scrollytell_example_server("scrollytell_example") } diff --git a/R/app_ui.R b/R/app_ui.R index dea3e8a..b26c70a 100644 --- a/R/app_ui.R +++ b/R/app_ui.R @@ -26,16 +26,20 @@ app_ui <- function(request) { well = FALSE, widths = c(3, 9), tabPanel( - title = "Introduction", - mod_markdown_example_ui("markdown_example_ui_1") + title = "Markdown cheat sheet", + mod_markdown_example_ui("markdown_cheat_sheet_example") + ), + tabPanel( + title = "Another markdown page", + mod_markdown_internal_link_example_ui("markdown_internal_link_example") ), tabPanel( title = "Charts", - mod_chart_example_ui("chart_example_ui_1") + mod_chart_example_ui("chart_example") ), tabPanel( title = "Scrolly example", - mod_scrollytell_example_ui("scrollytell_example_1") + mod_scrollytell_example_ui("scrollytell_example") ) ) ) From dbc50ebac71494126f8b57d041a8b3b257e0b13d Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:18:43 +0100 Subject: [PATCH 03/32] Add markdown + JS for internal links --- .../assets/markdown/mod_markdown_example.md | 122 ++++++++++++++++-- .../mod_markdown_internal_link_example.md | 17 +++ inst/app/www/js/custom.js | 78 +++++++++++ 3 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 inst/app/www/assets/markdown/mod_markdown_internal_link_example.md diff --git a/inst/app/www/assets/markdown/mod_markdown_example.md b/inst/app/www/assets/markdown/mod_markdown_example.md index d0ea8c0..e0d89ad 100644 --- a/inst/app/www/assets/markdown/mod_markdown_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_example.md @@ -1,17 +1,117 @@ -# Header 1 +## Markdown cheat sheet -## Header 2 +### Basic syntax -Inline **bold** or *italics* +These are the elements outlined in John Gruber's original design document. All Markdown applications support these elements. -Ordered lists: +#### Heading -1. one -2. two -3. three +# H1 +## H2 +### H3 -Unordered lists: +#### Bold -* one -* two -* three \ No newline at end of file +Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. + +__bold text__ + +#### Italic + +Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. + +_italicized text_ + +#### Blockquote + +> blockquote + +#### Ordered list + +1. First item +2. Second item +3. Third item + +#### Unordered list + +- First item +- Second item +- Third item + +#### Code + +A single pair of backticks is generally used. But the review scripts require 4 pairs! + +````code```` + +#### Horizontal rule + +--- + +#### External link + +[Markdown Guide](https://www.markdownguide.org) + +#### Internal link + +You can link to [another page](http://127.0.0.1/Another_markdown_page), and even a [specific heading](http://127.0.0.1/Another_markdown_page?linked-heading) on another page. The browser back button will also be enabled on doing this, to allow you to get back to where you were. + +#### Image + +Always specify the alt text. You can include images from web sources... + +![alt text](https://cdn.ons.gov.uk/assets/images/ons-logo/v2/ons-logo.svg) + +...and images served locally. + +![alt text](www/assets/logos/nhs-logo.png) + +### Extended syntax + +These elements extend the basic syntax by adding additional features. Not all Markdown applications support these elements. Only the ones supported when using ````shiny::includeMarkdown```` are shown. + +#### Table + +| Syntax | Description | +| ----------| ----------- | +| Header | Title | +| Paragraph | Text | + +#### Fenced code block + +```` +{ + "firstName": "John", + "lastName": "Smith", + "age": 25 +} +```` + +#### Footnote + +Here's a sentence with a footnote. [^1] + +[^1]: This is the footnote. + +#### Definition list + +term +: definition + +#### Strikethrough + +~~The world is flat.~~ + +#### Task list + +- [x] Write the press release +- [ ] Update the website +- [ ] Contact the media + +#### Subscript + +H~2~O + +#### Superscript + +X^2^ diff --git a/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md b/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md new file mode 100644 index 0000000..b6cd43e --- /dev/null +++ b/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md @@ -0,0 +1,17 @@ +## Another markdown page + +This page is linked to from the Markdown Cheat Sheet page. There is a heading below the fake latin for demonstration of internal links... + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vel erat ac ante aliquet pellentesque. Phasellus consectetur euismod purus, eu convallis erat malesuada eget. In tristique risus neque. Sed nulla felis, dignissim ut tortor ac, volutpat elementum ex. Fusce efficitur ante vel erat commodo, vel sodales odio sollicitudin. Nulla molestie odio id pretium cursus. Phasellus ornare quam eget metus vehicula vulputate. + +In dignissim semper dignissim. Maecenas libero mi, fermentum at diam sed, aliquam tincidunt tellus. Donec sollicitudin ullamcorper justo. Duis tincidunt urna sapien, a ornare ligula maximus id. Aliquam placerat turpis non ligula suscipit luctus. Fusce pellentesque accumsan placerat. Donec at mauris eu diam semper tempor. + +Vivamus augue magna, congue at molestie in, vulputate ut lacus. Donec sagittis tortor sit amet ex gravida, sed ullamcorper ex pretium. Sed at tincidunt metus. Maecenas luctus nec eros ullamcorper scelerisque. Fusce eget dolor eu dolor sodales lacinia. Quisque iaculis lacus in bibendum fermentum. Donec porta mauris quis eros posuere fringilla. + +Nulla molestie nibh egestas euismod imperdiet. Proin congue faucibus enim, ac condimentum diam fringilla id. In ac auctor odio. Sed eu magna a velit accumsan ornare vitae a dolor. Vestibulum laoreet eu ex nec vulputate. Nullam rutrum imperdiet mauris, a ullamcorper erat efficitur ac. Vestibulum ut gravida turpis. Donec at venenatis mi. Maecenas a velit lorem. Vivamus quis efficitur urna. Aliquam erat volutpat. Phasellus pretium est tellus, in accumsan orci laoreet quis. Cras fermentum, libero nec elementum egestas, quam mi tincidunt ligula, eu efficitur metus justo non felis. + +Suspendisse nec consequat nulla, ac posuere velit. Integer interdum commodo lorem, sit amet luctus felis tincidunt varius. Donec id tortor quis orci bibendum congue sit amet eu velit. Aenean sapien nulla, congue sed aliquam eget, porttitor non nibh. Sed tincidunt sit amet sem ac pulvinar. Phasellus non ligula volutpat, tincidunt lectus pulvinar, pretium tortor. Integer non mattis nisl. Etiam eget nisl elementum, auctor libero vitae, ornare est. Morbi orci lorem, consequat id semper venenatis, lacinia nec ipsum. + +### Linked heading + +Headings will automatically be assigned an id created from the heading text. Only alphanumeric characters will be retained, and they will be made lower-case and separated by dashes. So the above heading will be, in HTML, ````

Linked heading

````. diff --git a/inst/app/www/js/custom.js b/inst/app/www/js/custom.js index b360fdc..fe539b1 100644 --- a/inst/app/www/js/custom.js +++ b/inst/app/www/js/custom.js @@ -22,4 +22,82 @@ $(document).ready(function () { forEach(function(element){ observer.observe(element, config); }); + + // Add target='_blank' to external links so they open a new tab + // Add internal link behaviour to internal links + $(document.body).on('mouseover', 'a[role!=tab][target!=_blank]:not(.local)', function (e) { + var a = $(this); + if ( + !a.attr('href').match(/^mailto\:/) + && (a[0].hostname != window.location.hostname) + && !a.attr('href').match(/^javascript\:/) + && !a.attr('href').match(/^$/) + ) { + // External link + a.attr('target', '_blank'); + } else { + // Internal link + var tabName = a.attr('href').split("/")[3].split("?")[0].replace(/_/g, ' '); + var id = a.attr('href').split("?")[1] ?? ''; + + a.addClass('local'). + removeAttr('href'). + attr('onclick', 'internalLink("' + tabName + '", "' + id + '");'); + } + }); }); + + +// Go to specified tab and scroll position +var gotoTabPos = function(tab, pos) { + $('#mainTabs a[data-value=\"' + tab + '\"]').click(); + $(window).scrollTop(pos); + + // reset tracking vars to null + tabOnClick = null; + posOnClick = null; +}; + +// global vars to track tab and position when internal link is clicked +var tabOnClick = null; +var posOnClick = null; + +// Assign function to back browser button +window.onpopstate = function() { + gotoTabPos(tabOnClick, posOnClick); + + // Replace current history state with current URL, to prevent back/forward + // buttons affecting page until next time internal link is clicked + setTimeout(function () { + history.replaceState(null , '', window.location.href) + }, 1000); +}; + + +// Internal links - allows to navigate directly to a given tab and id +// Saves tab and page position when click occurs, to enable going back to same +var internalLink = function(tabName, id) { + // Save current tab and position + tabOnClick = $('#mainTabs li.active a').attr('data-value'); + posOnClick = $(window).scrollTop(); + + // Initialise history to enable back button + history.pushState({}, ''); + + var tabList = document.getElementsByTagName('a'); + for (var i = 0; i < tabList.length; i++) { + var tab = tabList[i]; + if(tab.getAttribute("data-value") == tabName) { + tab.click(); + + // exit early if no id was specified + if ($id.length === 0) { + return; + } + + document.getElementById(id).scrollIntoView({ + behavior: 'smooth' + }); + }; + } +}; From fab6847d2e2f568c6b5fc048ed68ac35b5c596cf Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:19:08 +0100 Subject: [PATCH 04/32] Add new module for additional markdown page --- R/mod_markdown_internal_link_example.R | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 R/mod_markdown_internal_link_example.R diff --git a/R/mod_markdown_internal_link_example.R b/R/mod_markdown_internal_link_example.R new file mode 100644 index 0000000..0afd8c2 --- /dev/null +++ b/R/mod_markdown_internal_link_example.R @@ -0,0 +1,22 @@ +#' markdown_internal_link_example UI Function +#' +#' @description A shiny Module. +#' +#' @param id,input,output,session Internal parameters for {shiny}. +#' +#' @noRd +mod_markdown_internal_link_example_ui <- function(id) { + ns <- NS(id) + tagList( + includeMarkdown("inst/app/www/assets/markdown/mod_markdown_internal_link_example.md") + ) +} + +#' markdown_example Server Functions +#' +#' @noRd +mod_markdown_internal_link_example_server <- function(id) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + }) +} From 6888c21b35649173f2303f1b60e121055aeabcee Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:20:27 +0100 Subject: [PATCH 05/32] No intentional change, cannot see that anything has... --- man/nhs_selectizeInput.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/nhs_selectizeInput.Rd b/man/nhs_selectizeInput.Rd index de1ab9f..fe0b0e1 100644 --- a/man/nhs_selectizeInput.Rd +++ b/man/nhs_selectizeInput.Rd @@ -24,7 +24,7 @@ from the server. For example, long lists of organisations or drugs. It allows to search for options by typing as well as dropdown. } \examples{ -# In module UI function +# In module UI function nhs_selectizeInput( "fruit", "Choose some fruit", From d291e6a676f52ffbb09bd3726da3630e07a6295d Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:21:30 +0100 Subject: [PATCH 06/32] Bump version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a058292..05a30da 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: nhsbsaShinyR Title: Template for NHSBSA {shiny} apps -Version: 0.0.0.9002 +Version: 0.0.0.9003 Authors@R: c(person(given = "DALL", family = "", From 617545f55f95e33bf676b80483cf023ec26941c0 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 11:53:01 +0100 Subject: [PATCH 07/32] Remove div wrapper from nhs_card_tabstop; it was preventing the chart from displaying --- R/utils_accessibility.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils_accessibility.R b/R/utils_accessibility.R index 6c3b075..2cff161 100644 --- a/R/utils_accessibility.R +++ b/R/utils_accessibility.R @@ -121,6 +121,6 @@ h6_tabstop <- function(header, tabindex = 0, ...) { nhs_card_tabstop <- function(header, tabindex = 0, ...) { # create nhs_card as typical nhs_card plus tabindex attribute # ensures nhs_card will be stopped at when pressing keyboard tab - nhs_card_tabstop <- div(nhs_card(header, ...)) %>% + nhs_card_tabstop <- nhs_card(header, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } From f651b51eb7de805f0111792daf8e3327f566df3a Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 14:35:57 +0100 Subject: [PATCH 08/32] Tidy up markdown examples + remove any not compatible syntax --- .../assets/markdown/mod_markdown_example.md | 136 +++++++++--------- .../mod_markdown_internal_link_example.md | 12 +- inst/app/www/js/custom.js | 2 +- 3 files changed, 83 insertions(+), 67 deletions(-) diff --git a/inst/app/www/assets/markdown/mod_markdown_example.md b/inst/app/www/assets/markdown/mod_markdown_example.md index e0d89ad..5023a98 100644 --- a/inst/app/www/assets/markdown/mod_markdown_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_example.md @@ -1,117 +1,125 @@ ## Markdown cheat sheet -### Basic syntax +The markdown syntax compatible with the review automation scripts is shown below. There exists further markdown syntax that could be incorporated if required. -These are the elements outlined in John Gruber's original design document. All Markdown applications support these elements. +--- -#### Heading +### Heading +``` # H1 ## H2 ### H3 +#### H4 +``` -#### Bold +# H1 +## H2 +### H3 +#### H4 -Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. +--- + +### Bold + +Original markdown used double * characters, but underscore can be used, and is what is used for the review scripts to work. +``` __bold text__ +``` -#### Italic +__bold text__ + +--- + +### Italic Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. +``` _italicized text_ +``` -#### Blockquote +_italicized text_ -> blockquote +--- -#### Ordered list +### Ordered list +``` 1. First item 2. Second item 3. Third item +``` + +1. First item +2. Second item +3. Third item + +--- -#### Unordered list +### Unordered list +``` - First item - Second item - Third item +``` -#### Code - -A single pair of backticks is generally used. But the review scripts require 4 pairs! - -````code```` - -#### Horizontal rule +- First item +- Second item +- Third item --- -#### External link - -[Markdown Guide](https://www.markdownguide.org) - -#### Internal link +### Inline code -You can link to [another page](http://127.0.0.1/Another_markdown_page), and even a [specific heading](http://127.0.0.1/Another_markdown_page?linked-heading) on another page. The browser back button will also be enabled on doing this, to allow you to get back to where you were. +``` +Here is some inline ````code````. Note it uses four pairs of backticks, not a single pair as usual for markdown! +``` -#### Image +Here is some inline `code`. -Always specify the alt text. You can include images from web sources... - -![alt text](https://cdn.ons.gov.uk/assets/images/ons-logo/v2/ons-logo.svg) - -...and images served locally. - -![alt text](www/assets/logos/nhs-logo.png) - -### Extended syntax - -These elements extend the basic syntax by adding additional features. Not all Markdown applications support these elements. Only the ones supported when using ````shiny::includeMarkdown```` are shown. +--- -#### Table +### External link -| Syntax | Description | -| ----------| ----------- | -| Header | Title | -| Paragraph | Text | +``` +[Markdown Guide](https://www.markdownguide.org) +``` -#### Fenced code block +[Markdown Guide](https://www.markdownguide.org) -```` -{ - "firstName": "John", - "lastName": "Smith", - "age": 25 -} -```` +--- -#### Footnote +### Internal link -Here's a sentence with a footnote. [^1] +You can link to another page using the `localhost` IP address and pointing to a page by using the `title` of its `tabpanel`, as defined in `app_ui.R`, with any spaces replaced by underscores. -[^1]: This is the footnote. +``` +[Link to page "Another markdown page"](http://127.0.0.1/Another_markdown_page) +``` -#### Definition list +[Link to page "Another markdown page"](http://127.0.0.1/Another_markdown_page) -term -: definition +You can even link to a specific heading on another page. Just add a `?` followed by a string formed from the lower case heading text, with non-alphanumeric characters removed and spaces replaced with dashes. -#### Strikethrough +``` +[Link to heading "Linked heading"on page "Another markdown page"](http://127.0.0.1/Another_markdown_page?linked-heading) +``` -~~The world is flat.~~ +[Link to heading "Linked heading" on page "Another markdown page"](http://127.0.0.1/Another_markdown_page?linked-heading) -#### Task list +The browser back button will also be enabled after using an internal link, to allow you to get back to where you were. -- [x] Write the press release -- [ ] Update the website -- [ ] Contact the media +### Inline code with link -#### Subscript +Links can be created on inline code using -H~2~O +``` +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) +``` -#### Superscript +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) -X^2^ +Note the braces (`{}`) are not necessary, but are a convention when writing an R package name. diff --git a/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md b/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md index b6cd43e..fde4a19 100644 --- a/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md @@ -1,6 +1,8 @@ ## Another markdown page -This page is linked to from the Markdown Cheat Sheet page. There is a heading below the fake latin for demonstration of internal links... +This page is linked to from the Markdown cheat sheet page. There is a heading below the fake latin for demonstration of internal links. If you got here by clicking the link, the back button on the browser will be enabled to take you back. + +--- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vel erat ac ante aliquet pellentesque. Phasellus consectetur euismod purus, eu convallis erat malesuada eget. In tristique risus neque. Sed nulla felis, dignissim ut tortor ac, volutpat elementum ex. Fusce efficitur ante vel erat commodo, vel sodales odio sollicitudin. Nulla molestie odio id pretium cursus. Phasellus ornare quam eget metus vehicula vulputate. @@ -12,6 +14,12 @@ Nulla molestie nibh egestas euismod imperdiet. Proin congue faucibus enim, ac co Suspendisse nec consequat nulla, ac posuere velit. Integer interdum commodo lorem, sit amet luctus felis tincidunt varius. Donec id tortor quis orci bibendum congue sit amet eu velit. Aenean sapien nulla, congue sed aliquam eget, porttitor non nibh. Sed tincidunt sit amet sem ac pulvinar. Phasellus non ligula volutpat, tincidunt lectus pulvinar, pretium tortor. Integer non mattis nisl. Etiam eget nisl elementum, auctor libero vitae, ornare est. Morbi orci lorem, consequat id semper venenatis, lacinia nec ipsum. +--- + ### Linked heading -Headings will automatically be assigned an id created from the heading text. Only alphanumeric characters will be retained, and they will be made lower-case and separated by dashes. So the above heading will be, in HTML, ````

Linked heading

````. +Headings will automatically be assigned an id created from the heading text. Only alphanumeric characters will be retained, and they will be made lower-case and separated by dashes. So the above heading will be, in HTML + +`

Linked heading

` + +If you got here by clicking the link, the back button on the browser will be enabled to take you back. diff --git a/inst/app/www/js/custom.js b/inst/app/www/js/custom.js index fe539b1..ff082a2 100644 --- a/inst/app/www/js/custom.js +++ b/inst/app/www/js/custom.js @@ -91,7 +91,7 @@ var internalLink = function(tabName, id) { tab.click(); // exit early if no id was specified - if ($id.length === 0) { + if (id.length === 0) { return; } From 390ea0836eb416be260ca44b365ecaf55e1ccbc8 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 14:37:49 +0100 Subject: [PATCH 09/32] Add further Suggests --- DESCRIPTION | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 05a30da..19f7a10 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,8 +40,17 @@ Imports: shiny, shinyjs Suggests: + officedown, + officer, pkgload, - usethis + purrr, + rmarkdown, + rprojroot, + testthat, + tidyr, + usethis, + withr, + xml2 Remotes: nhsbsa-data-analytics/nhsbsaR, statistiekcbs/scrollytell From 83fd3297d8c486869f88b7955a7051d45ea1dc15 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 27 Oct 2023 14:41:13 +0100 Subject: [PATCH 10/32] Add review folder and code --- review/scripts/md_to_word.R | 54 ++++ review/scripts/word_to_md.R | 443 +++++++++++++++++++++++++++++++++ review/styles/draft-styles.rmd | 28 +++ review/tests/snapshot_tests.R | 111 +++++++++ 4 files changed, 636 insertions(+) create mode 100644 review/scripts/md_to_word.R create mode 100644 review/scripts/word_to_md.R create mode 100644 review/styles/draft-styles.rmd create mode 100644 review/tests/snapshot_tests.R diff --git a/review/scripts/md_to_word.R b/review/scripts/md_to_word.R new file mode 100644 index 0000000..22d61e8 --- /dev/null +++ b/review/scripts/md_to_word.R @@ -0,0 +1,54 @@ +library(rmarkdown) +library(officedown) +library(purrr) + +# Setup ------------------------------------------------------------------- + +md_dir <- "inst/app/www/assets/markdown" # Markdown dir +rv_dir <- "review" # Review directory +rmd_file <- "all_md.rmd" # Output rmarkdown +docx_file <- "review.docx" # Output Word doc +styles_dir <- "review/styles" # Styles dir +styles_rmd <- "draft-styles.rmd" # Creates Word template +styles_doc <- "draft-styles.docx" # Word template + +render( + file.path(styles_dir, styles_rmd), + output_dir = styles_dir, + output_file = styles_doc, + quiet = TRUE +) + + +# Create rmarkdown -------------------------------------------------------- + +# Get relative path of md files +md_files <- Sys.glob(file.path(md_dir, "*.md")) + +# Combine them into one rmarkdown file +all_md <- map(md_files, readLines) # List of md content +all_md <- map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add marker to md file +all_md <- reduce(all_md, c) # All content in one vector + +writeLines(all_md, file.path(rv_dir, rmd_file)) + + +# Create Word document ---------------------------------------------------- + +render( + file.path(rv_dir, rmd_file), + rdocx_document( + reference_docx = file.path(styles_dir, styles_doc), + toc = FALSE, + number_sections = FALSE + ), + output_dir = rv_dir, + output_file = docx_file, + quiet = TRUE +) + + +# Tidy up ----------------------------------------------------------------- + +unlink(file.path(styles_dir, styles_doc)) # Delete style doc +unlink(file.path(rv_dir, rmd_file)) # Delete combined rmd diff --git a/review/scripts/word_to_md.R b/review/scripts/word_to_md.R new file mode 100644 index 0000000..9b8e134 --- /dev/null +++ b/review/scripts/word_to_md.R @@ -0,0 +1,443 @@ +library(officer) +library(purrr) +library(dplyr) +library(tidyr) +library(xml2) + +# Setup ------------------------------------------------------------------- + +md_dir <- "inst/app/www/assets/markdown" # Markdown dir +rv_dir <- "review" # Review directory +docx_file <- "review.docx" # Input Word doc +md_out_dir <- "review/temp" # Output markdown dir + + +# Get Word doc contents --------------------------------------------------- + +doc <- read_docx(file.path(rv_dir, docx_file)) +doc_df <- docx_summary(doc) + + +# Get style data ---------------------------------------------------------- + +# We need to create maps for each type of styling or hyperlink. +# These will be applied to the markdown generated from the Word doc +style_map <- function(doc, t1, t2 = NULL, t3 = NULL, val3 = NULL, t4 = NULL, val4 = NULL) { + smap <- list() + num_blank <- 0 + num_elements <- length(doc) + + for (i in 1:num_elements) { + doc$officer_cursor$which <- i + num_chars <- 0 + parent <- docx_current_block_xml(doc) + first_children <- xml_children(parent) + if(xml_text(parent) == "") num_blank <- num_blank + 1 + for (first_child in first_children) { + match_found <- FALSE + num_chars <- num_chars + nchar(xml_text(first_child)) + if (!xml_name(first_child) == t1) next + if (!is.null(t2)) { + second_children <- xml_children(first_child) + for (second_child in second_children) { + if (!xml_name(second_child) == t2) next + if (!is.null(t3)) { + third_children <- xml_children(second_child) + for (third_child in third_children) { + if (!xml_name(third_child) == t3) next + if (!is.null(val3) && is.na(xml_attr(third_child, "val"))) next + if (!is.null(val3) && xml_attr(third_child, "val") != val3) next + if (!is.null(t4)) { + fourth_children <- xml_children(third_child) + for (fourth_child in fourth_children) { + if (!xml_name(fourth_child) == t4) next + if (!is.null(val4) && is.na(xml_attr(fourth_child, "val"))) next + if (xml_attr(fourth_child, "val") != val4) next + match_found <- TRUE + } + } else match_found <- TRUE + } + } else match_found <- TRUE + } + } else match_found <- TRUE + + if (match_found) { + style_data <- list( + num_chars - nchar(xml_text(first_child)) + 1, + num_chars + ) + xml_attr_names <- xml_attrs(first_child) %>% names() + if (length(xml_attr_names) & "id" %in% xml_attr_names) { + style_data <- c(style_data, list(xml_attrs(first_child)[["id"]])) + } + if (length(xml_attr_names) & "anchor" %in% xml_attr_names) { + style_data <- c(style_data, list(xml_attrs(first_child)[["anchor"]])) + } + if (as.character(i - num_blank) %in% names(smap)) { + prev_index <- length(smap[[as.character(i - num_blank)]]) + new_index <- prev_index + 1 + if (style_data[[1]] == + smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { + smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- style_data[[2]] + } else { + smap[[as.character(i - num_blank)]][[new_index]] <- style_data + } + } else { + smap[[as.character(i - num_blank)]] <- list( + `1` = style_data + ) + } + } + } + } + + smap %>% map(unique) +} + +maps <- list( + bold_map = style_map(doc, "r", "rPr", "b"), + ital_map = style_map(doc, "r", "rPr", "i"), + hypl_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "Hyperlink"), + code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), + chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") +) + + +# Compute file breakpoints ------------------------------------------------ + +# This will find the rows to use for each md file +breaks <- doc_df %>% + filter(startsWith(text, md_dir)) %>% + mutate( + md_file = text, + begin = doc_index + 1, + end = lead(doc_index) - 1, + .keep = "none" + ) %>% + replace_na(list(end = nrow(doc_df))) + + +# Create markdown files --------------------------------------------------- + +# Iterate over the markdown filenames. The content for each file is transformed +# to apply styling and hyperlinks and then written to md_out_dir +pwalk( + breaks, + \(md_file, begin, end) { + # Each file has content from row number start to end + doc_df <- doc_df %>% + filter(between(doc_index, begin, end)) + + # Apply any bold styling + for (row in intersect(names(maps$bold_map), begin:end)) { + offset <- 0 + increment <- 4 + for (style_data in maps$bold_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + bold_applied <- doc_df %>% + filter(doc_index == rownum) %>% + mutate( + text = paste0( + substr(text, 0, start - 1), + "__", + substr(text, start, stop), + "__", + substr(text, stop + 1, nchar(text)) + ) + ) %>% + pull(text) + + doc_df <- doc_df %>% + mutate( + text = replace( + text, + doc_index == rownum, + bold_applied + ) + ) + + # Add offsets to remaining maps + for (m in 2:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[i]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Apply any italics styling + for (row in intersect(names(maps$ital_map), begin:end)) { + offset <- 0 + increment <- 2 + for (style_data in maps$ital_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + ital_applied <- doc_df %>% + filter(doc_index == rownum) %>% + mutate( + text = paste0( + substr(text, 0, start - 1), + "_", + substr(text, start, stop), + "_", + substr(text, stop + 1, nchar(text)) + ) + ) %>% + pull(text) + + doc_df <- doc_df %>% + mutate( + text = replace( + text, + doc_index == rownum, + ital_applied + ) + ) + + # Add offsets to remaining maps + for (m in 3:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Add any hyperlinks + for (row in intersect(names(maps$hypl_map), begin:end)) { + offset <- 0 + increment <- 4 + for (style_data in maps$hypl_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + invisible(capture.output( + url <- doc$ + doc_obj$ + relationship()$ + show() %>% + as_tibble() %>% + filter(id == style_data[[3]]) %>% + pull(target) + )) + url <- if (length(style_data) == 4) { + paste0(url, "#", style_data[[4]]) + } else { + url + } + + hypl_applied <- doc_df %>% + filter(doc_index == rownum) %>% + mutate( + text = paste0( + substr(text, 0, start - 1), + "[", + substr(text, start, stop), + "](", + url, + ")", + substr(text, stop + 1, nchar(text)) + ) + ) %>% + pull(text) + + doc_df <- doc_df %>% + mutate( + text = replace( + text, + doc_index == rownum, + hypl_applied + ) + ) + + # Add offsets to remaining maps + for (m in 4:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + nchar(url) + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + nchar(url) + } + } + } + } + + offset <- offset + increment + nchar(url) + } + } + + # Apply any code (monospace font) styling + for (row in intersect(names(maps$code_map), begin:end)) { + offset <- 0 + increment <- 8 + for (style_data in maps$code_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + code_applied <- doc_df %>% + filter(doc_index == rownum) %>% + mutate( + text = paste0( + substr(text, 0, start - 1), + "````", + substr(text, start, stop), + "````", + substr(text, stop + 1, nchar(text)) + ) + ) %>% + pull(text) + + doc_df <- doc_df %>% + mutate( + text = replace( + text, + doc_index == rownum, + code_applied + ) + ) + + # Add offsets to remaining maps + for (m in 5:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Add any code (monospace font) hyperlinks + for (row in intersect(names(maps$chyp_map), begin:end)) { + offset <- 0 + increment <- 12 + for (style_data in maps$chyp_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + invisible(capture.output( + url <- doc$ + doc_obj$ + relationship()$ + show() %>% + as_tibble() %>% + filter(id == style_data[[3]]) %>% + pull(target) + )) + + chyp_applied <- doc_df %>% + filter(doc_index == rownum) %>% + mutate( + text = paste0( + substr(text, 0, start - 1), + "[````", + substr(text, start, stop), + "````](", + url, + ")", + substr(text, stop + 1, nchar(text)) + ) + ) %>% + pull(text) + + doc_df <- doc_df %>% + mutate( + text = replace( + text, + doc_index == rownum, + chyp_applied + ) + ) + + offset <- offset + increment + nchar(url) + } + } + + # Numbered lists need special treatment, the XML is very convoluted... + numbering_xml <- file.path( + doc$package_dir, + "word", + "numbering.xml" + ) %>% read_xml() + + num_ids_alt_1 <- numbering_xml %>% + xml_find_all("//w:num[w:abstractNumId/@w:val='99411']") %>% + xml_attr("numId") %>% + as.numeric() + num_ids_alt_2 <- numbering_xml %>% + xml_find_all("//w:num[w:abstractNumId/@w:val='2']") %>% + xml_attr("numId") %>% + as.numeric() + num_ids <- c(num_ids_alt_1, num_ids_alt_2) + + # Add heading, bullet and list markup and find where to add blank lines + doc_df <- doc_df %>% + mutate( + next_style = lead(style_name), + blank_after = ( + (style_name != "Compact") | + (style_name == "Compact" & lead(style_name) != "Compact") + ) & + (!is.na(lead(style_name))), + style_name = case_when( + num_id %in% num_ids ~ "Numbering", + TRUE ~ style_name + ), + text = case_match( + style_name, + "Heading 2" ~ paste0("## ", text), + "heading 2" ~ paste0("## ", text), + "Heading 3" ~ paste0("### ", text), + "heading 3" ~ paste0("### ", text), + "Heading 4" ~ paste0("#### ", text), + "heading 4" ~ paste0("#### ", text), + "Compact" ~ paste0("- ", text), + .default = text + ) + ) %>% + group_by(num_id) %>% + mutate( + text = case_match( + style_name, + "Numbering" ~ paste0(seq(n()), ". ", text), + .default = text + ) + ) %>% + ungroup() + + # Get the element indices which need a blank line afterward + needs_blank_after <- which(doc_df$blank_after) + + seq_len(length(which(doc_df$blank_after))) - 1 + + # Iterate over the indices and add a new blank row after each + walk(needs_blank_after, \(x) doc_df <<- add_row(doc_df, text = "", .after = x)) + + # Write the markdown file + writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) + } +) diff --git a/review/styles/draft-styles.rmd b/review/styles/draft-styles.rmd new file mode 100644 index 0000000..7bf2b77 --- /dev/null +++ b/review/styles/draft-styles.rmd @@ -0,0 +1,28 @@ +--- +title: "Untitled" +output: word_document +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = TRUE) +``` + +## R Markdown + +This is an R Markdown document. Markdown is a simple formatting syntax for authoring HTML, PDF, and MS Word documents. For more details on using R Markdown see . + +When you click the **Knit** button a document will be generated that includes both content as well as the output of any embedded R code chunks within the document. You can embed an R code chunk like this: + +```{r cars} +summary(cars) +``` + +## Including Plots + +You can also embed plots, for example: + +```{r pressure, echo=FALSE} +plot(pressure) +``` + +Note that the `echo = FALSE` parameter was added to the code chunk to prevent printing of the R code that generated the plot. diff --git a/review/tests/snapshot_tests.R b/review/tests/snapshot_tests.R new file mode 100644 index 0000000..fd91456 --- /dev/null +++ b/review/tests/snapshot_tests.R @@ -0,0 +1,111 @@ +library(testthat) +library(withr) +library(rprojroot) + +local_edition(3) + +# INSTRUCTIONS +# 1. Get initial snapshots +# 2. Comment out the first code block +# 3. Uncomment the rest of the code +# 4. Whenever you make changes to these files, 'Run Tests' again +# 5. Any differences will be caught; run the below code in the console to review +# them in a shiny app (working dir is project root): +# testthat::snapshot_review('snapshot_tests/', "review/tests") +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# If there are some changes that were not intended, you should fix these in +# the source files. You can leave the diff viewer open and revert the changes +# directly by a simple copy and paste of the original content. Click 'Skip' +# once the reversions are complete. +# 7. Close the diff viewer once only skipped files remain. +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. +# 9. If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, uncomment it now so it is ready for the next set of +# changes. + +# 1. To get the initial snapshot, run the below block, with an expectation for +# each reference file. Use 'Run Tests' button above right. +# You are basically saying "all these files should look as they are now in +# future". +test_that("generate snapshots", { + with_dir(find_package_root_file(), { + expect_snapshot_file("inst/markdown/01_headline_figures.md") + expect_snapshot_file("inst/markdown/02_patients_age_gender.md") + expect_snapshot_file("inst/markdown/03_patients_imd.md") + expect_snapshot_file("inst/markdown/04_metrics_ch_type.md") + expect_snapshot_file("inst/markdown/05_metrics_age_gender.md") + expect_snapshot_file("inst/markdown/06_geo_ch_flag.md") + expect_snapshot_file("inst/markdown/07_ch_flag_drug.md") + expect_snapshot_file("inst/markdown/08_geo_ch_flag_drug.md") + expect_snapshot_file("inst/markdown/09_metrics_1.md") + expect_snapshot_file("inst/markdown/09_metrics_2.md") + expect_snapshot_file("inst/markdown/09_metrics_3.md") + expect_snapshot_file("inst/markdown/09_metrics_4.md") + expect_snapshot_file("inst/markdown/10_datasets.md") + expect_snapshot_file("inst/markdown/11_address_matching.md") + expect_snapshot_file("inst/markdown/12_feedback.md") + expect_snapshot_file("inst/markdown/13_annex.md") + expect_snapshot_file("inst/markdown/final_thoughts.md") + }) +}) + +# 2. Comment the above block out now + +# 3. Uncomment the below block, ready for future changes + +# 4. When you make changes to the files, use 'Run Tests' button above right +# test_that("markdown text is as expected", { +# with_dir(find_package_root_file(), { +# source("review/scripts/word_to_md.R") +# +# expect_snapshot_file("review/temp/01_headline_figures.md") +# expect_snapshot_file("review/temp/02_patients_age_gender.md") +# expect_snapshot_file("review/temp/03_patients_imd.md") +# expect_snapshot_file("review/temp/04_metrics_ch_type.md") +# expect_snapshot_file("review/temp/05_metrics_age_gender.md") +# expect_snapshot_file("review/temp/06_geo_ch_flag.md") +# expect_snapshot_file("review/temp/07_ch_flag_drug.md") +# expect_snapshot_file("review/temp/08_geo_ch_flag_drug.md") +# expect_snapshot_file("review/temp/09_metrics_1.md") +# expect_snapshot_file("review/temp/09_metrics_2.md") +# expect_snapshot_file("review/temp/09_metrics_3.md") +# expect_snapshot_file("review/temp/09_metrics_4.md") +# expect_snapshot_file("review/temp/10_datasets.md") +# expect_snapshot_file("review/temp/11_address_matching.md") +# expect_snapshot_file("review/temp/12_feedback.md") +# expect_snapshot_file("review/temp/13_annex.md") +# expect_snapshot_file("review/temp/final_thoughts.md") +# }) +# }) + +# 5. If any differences are present (there should be if you changed the +# files...) a shiny app can be started showing the differences by running the +# code below in the console (you can open it in browser using the 'Show in new +# window' button in the Viewer pane) +# testthat::snapshot_review('snapshot_tests/', "review/tests") + +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# If there are some changes that were not intended, you should fix these in +# the source files. You can leave the diff viewer open and revert the changes +# directly by a simple copy and paste of the original content. Click 'Skip' +# once the reversions are complete. + +# 7. Close the diff viewer once only skipped files remain. + +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. + +# 9. If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, uncomment it now so it is ready for the next set of +# changes. From 2538cdf691333f00e4d18f70b19cc4101310a8a4 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Tue, 31 Oct 2023 16:00:16 +0000 Subject: [PATCH 11/32] Shorten long mod name + move comment on inline code out of code itself --- R/app_ui.R | 2 +- R/mod_internal_link_example.R | 22 +++++++++++++++++++ R/mod_markdown_internal_link_example.R | 22 ------------------- ...xample.md => mod_internal_link_example.md} | 0 .../assets/markdown/mod_markdown_example.md | 4 +++- 5 files changed, 26 insertions(+), 24 deletions(-) create mode 100644 R/mod_internal_link_example.R delete mode 100644 R/mod_markdown_internal_link_example.R rename inst/app/www/assets/markdown/{mod_markdown_internal_link_example.md => mod_internal_link_example.md} (100%) diff --git a/R/app_ui.R b/R/app_ui.R index b26c70a..4eeba89 100644 --- a/R/app_ui.R +++ b/R/app_ui.R @@ -31,7 +31,7 @@ app_ui <- function(request) { ), tabPanel( title = "Another markdown page", - mod_markdown_internal_link_example_ui("markdown_internal_link_example") + mod_internal_link_example_ui("internal_link_example") ), tabPanel( title = "Charts", diff --git a/R/mod_internal_link_example.R b/R/mod_internal_link_example.R new file mode 100644 index 0000000..a82a74e --- /dev/null +++ b/R/mod_internal_link_example.R @@ -0,0 +1,22 @@ +#' internal_link_example UI Function +#' +#' @description A shiny Module. +#' +#' @param id,input,output,session Internal parameters for {shiny}. +#' +#' @noRd +mod_internal_link_example_ui <- function(id) { + ns <- NS(id) + tagList( + includeMarkdown("inst/app/www/assets/markdown/mod_internal_link_example.md") + ) +} + +#' internal_link_example Server Function +#' +#' @noRd +mod_internal_link_example_server <- function(id) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + }) +} diff --git a/R/mod_markdown_internal_link_example.R b/R/mod_markdown_internal_link_example.R deleted file mode 100644 index 0afd8c2..0000000 --- a/R/mod_markdown_internal_link_example.R +++ /dev/null @@ -1,22 +0,0 @@ -#' markdown_internal_link_example UI Function -#' -#' @description A shiny Module. -#' -#' @param id,input,output,session Internal parameters for {shiny}. -#' -#' @noRd -mod_markdown_internal_link_example_ui <- function(id) { - ns <- NS(id) - tagList( - includeMarkdown("inst/app/www/assets/markdown/mod_markdown_internal_link_example.md") - ) -} - -#' markdown_example Server Functions -#' -#' @noRd -mod_markdown_internal_link_example_server <- function(id) { - moduleServer(id, function(input, output, session) { - ns <- session$ns - }) -} diff --git a/inst/app/www/assets/markdown/mod_markdown_internal_link_example.md b/inst/app/www/assets/markdown/mod_internal_link_example.md similarity index 100% rename from inst/app/www/assets/markdown/mod_markdown_internal_link_example.md rename to inst/app/www/assets/markdown/mod_internal_link_example.md diff --git a/inst/app/www/assets/markdown/mod_markdown_example.md b/inst/app/www/assets/markdown/mod_markdown_example.md index 5023a98..45ff780 100644 --- a/inst/app/www/assets/markdown/mod_markdown_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_example.md @@ -74,8 +74,10 @@ _italicized text_ ### Inline code +Inline code is usually made with a single pair of backticks. We need to use four pairs of backticks to allow it to work in the review code. + ``` -Here is some inline ````code````. Note it uses four pairs of backticks, not a single pair as usual for markdown! +Here is some inline ````code````. ``` Here is some inline `code`. From 81e71e4896f57a508f62d790a842eff80bcbd364 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Tue, 31 Oct 2023 17:24:04 +0000 Subject: [PATCH 12/32] Add vignette + rewording + small changes + update DESCRIPTION --- .Rbuildignore | 1 + DESCRIPTION | 2 + R/mod_internal_link_example.R | 2 +- .../assets/markdown/mod_markdown_example.md | 6 +- review/tests/snapshot_tests.R | 68 +++----- vignettes/.gitignore | 2 + vignettes/writing-and-reviewing-text.Rmd | 150 ++++++++++++++++++ 7 files changed, 182 insertions(+), 49 deletions(-) create mode 100644 vignettes/.gitignore create mode 100644 vignettes/writing-and-reviewing-text.Rmd diff --git a/.Rbuildignore b/.Rbuildignore index 73641e8..1e2f138 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,3 +11,4 @@ ^gitleaks.toml$ ^\.github$ ^\.lintr$ +^review$ diff --git a/DESCRIPTION b/DESCRIPTION index 19f7a10..364b1d3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,6 +40,7 @@ Imports: shiny, shinyjs Suggests: + knitr, officedown, officer, pkgload, @@ -57,3 +58,4 @@ Remotes: Encoding: UTF-8 LazyData: true RoxygenNote: 7.2.3 +VignetteBuilder: knitr diff --git a/R/mod_internal_link_example.R b/R/mod_internal_link_example.R index a82a74e..36f5190 100644 --- a/R/mod_internal_link_example.R +++ b/R/mod_internal_link_example.R @@ -15,7 +15,7 @@ mod_internal_link_example_ui <- function(id) { #' internal_link_example Server Function #' #' @noRd -mod_internal_link_example_server <- function(id) { +mod_internal_link_example_server <- function(id) { # Exclude Linting moduleServer(id, function(input, output, session) { ns <- session$ns }) diff --git a/inst/app/www/assets/markdown/mod_markdown_example.md b/inst/app/www/assets/markdown/mod_markdown_example.md index 45ff780..577178f 100644 --- a/inst/app/www/assets/markdown/mod_markdown_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_example.md @@ -6,14 +6,14 @@ The markdown syntax compatible with the review automation scripts is shown below ### Heading +Headings from level two to four can be used. First level heading is reserved for the main title, so should never appear in markdown files. + ``` -# H1 ## H2 ### H3 #### H4 ``` -# H1 ## H2 ### H3 #### H4 @@ -46,6 +46,8 @@ _italicized text_ ### Ordered list +Only single level lists are supported currently. + ``` 1. First item 2. Second item diff --git a/review/tests/snapshot_tests.R b/review/tests/snapshot_tests.R index fd91456..7b369fb 100644 --- a/review/tests/snapshot_tests.R +++ b/review/tests/snapshot_tests.R @@ -15,9 +15,9 @@ local_edition(3) # 6. Review the changes in each file one by one. # If ALL changes in a file are as intended, then choose the 'Accept' option. # If there are some changes that were not intended, you should fix these in -# the source files. You can leave the diff viewer open and revert the changes -# directly by a simple copy and paste of the original content. Click 'Skip' -# once the reversions are complete. +# the temporary markdown files (review/temp). You can leave the diff viewer +# open and revert the changes directly by a simple copy and paste of the +# original content. Click 'Skip' once the reversions are complete. # 7. Close the diff viewer once only skipped files remain. # 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line # source("review/scripts/word_to_md.R") @@ -35,23 +35,10 @@ local_edition(3) # future". test_that("generate snapshots", { with_dir(find_package_root_file(), { - expect_snapshot_file("inst/markdown/01_headline_figures.md") - expect_snapshot_file("inst/markdown/02_patients_age_gender.md") - expect_snapshot_file("inst/markdown/03_patients_imd.md") - expect_snapshot_file("inst/markdown/04_metrics_ch_type.md") - expect_snapshot_file("inst/markdown/05_metrics_age_gender.md") - expect_snapshot_file("inst/markdown/06_geo_ch_flag.md") - expect_snapshot_file("inst/markdown/07_ch_flag_drug.md") - expect_snapshot_file("inst/markdown/08_geo_ch_flag_drug.md") - expect_snapshot_file("inst/markdown/09_metrics_1.md") - expect_snapshot_file("inst/markdown/09_metrics_2.md") - expect_snapshot_file("inst/markdown/09_metrics_3.md") - expect_snapshot_file("inst/markdown/09_metrics_4.md") - expect_snapshot_file("inst/markdown/10_datasets.md") - expect_snapshot_file("inst/markdown/11_address_matching.md") - expect_snapshot_file("inst/markdown/12_feedback.md") - expect_snapshot_file("inst/markdown/13_annex.md") - expect_snapshot_file("inst/markdown/final_thoughts.md") + source("review/scripts/md_to_word.R") + + expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") + expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") }) }) @@ -64,38 +51,24 @@ test_that("generate snapshots", { # with_dir(find_package_root_file(), { # source("review/scripts/word_to_md.R") # -# expect_snapshot_file("review/temp/01_headline_figures.md") -# expect_snapshot_file("review/temp/02_patients_age_gender.md") -# expect_snapshot_file("review/temp/03_patients_imd.md") -# expect_snapshot_file("review/temp/04_metrics_ch_type.md") -# expect_snapshot_file("review/temp/05_metrics_age_gender.md") -# expect_snapshot_file("review/temp/06_geo_ch_flag.md") -# expect_snapshot_file("review/temp/07_ch_flag_drug.md") -# expect_snapshot_file("review/temp/08_geo_ch_flag_drug.md") -# expect_snapshot_file("review/temp/09_metrics_1.md") -# expect_snapshot_file("review/temp/09_metrics_2.md") -# expect_snapshot_file("review/temp/09_metrics_3.md") -# expect_snapshot_file("review/temp/09_metrics_4.md") -# expect_snapshot_file("review/temp/10_datasets.md") -# expect_snapshot_file("review/temp/11_address_matching.md") -# expect_snapshot_file("review/temp/12_feedback.md") -# expect_snapshot_file("review/temp/13_annex.md") -# expect_snapshot_file("review/temp/final_thoughts.md") +# expect_snapshot_file("review/temp/01_mod_the_first.md") +# expect_snapshot_file("review/temp/02_mod_the_second.md") # }) # }) -# 5. If any differences are present (there should be if you changed the -# files...) a shiny app can be started showing the differences by running the -# code below in the console (you can open it in browser using the 'Show in new -# window' button in the Viewer pane) +# 5. If any differences are present (there should be if you changed the files...) +# a diff viewer can be opened by running the code below in the console (you can +# open it in browser using the 'Show in new window' button in the Viewer pane) # testthat::snapshot_review('snapshot_tests/', "review/tests") # 6. Review the changes in each file one by one. # If ALL changes in a file are as intended, then choose the 'Accept' option. +# # If there are some changes that were not intended, you should fix these in # the source files. You can leave the diff viewer open and revert the changes -# directly by a simple copy and paste of the original content. Click 'Skip' -# once the reversions are complete. +# directly by a simple copy and paste of the original content. +# +# Click 'Skip' once the reversions are complete. # 7. Close the diff viewer once only skipped files remain. @@ -105,7 +78,10 @@ test_that("generate snapshots", { # differences to be found now, but if there are then repeat step 6 to 8 until # none are found. -# 9. If you had to comment out the line +# 9. It is now safe to copy the revised markdown files from the review/temp folder +# over the app markdown files in inst/app/www/assets/markdown. +# +# If you had to comment out the line # source("review/scripts/word_to_md.R") -# due to reversions, uncomment it now so it is ready for the next set of -# changes. +# due to reversions, MAKE SURE to uncomment it now so it is ready for the next +# set of changes. diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/writing-and-reviewing-text.Rmd b/vignettes/writing-and-reviewing-text.Rmd new file mode 100644 index 0000000..b5fdf5f --- /dev/null +++ b/vignettes/writing-and-reviewing-text.Rmd @@ -0,0 +1,150 @@ +--- +title: "Writing and reviewing text" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{writing-and-reviewing-text} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup, include = FALSE} +library(shiny) +``` + +## How text is included in the app + +All text is written in markdown (note, __not__ Rmarkdown but just plain old [markdown](https://www.markdownguide.org/cheat-sheet/)). Initially this was tried with Rmarkdown, but this caused various issues that were not a problem with plain markdown. + +The text markdown lives in the `inst/app/www/assets/markdown` folder. + +```{r} +list.files("../inst/app/www/assets/markdown") +``` + +Usually there is a single file for each module of the app. This is because in general all the text is first in the module, and followed by the graphical components. Sometimes that is not the case. For example, in the Care Homes update a [module for metrics](https://github.com/nhsbsa-data-analytics/care-home-prescribing-2020-2023/blob/main/R/mod_09_metrics.R) includes several tables interspersed in the text. We did try out using markdown created tables, but they did not look great. In the end we created them with `kable`, and use multiple markdown files. The code is below as an example, with most table code omitted for brevity. + +```{r, eval=FALSE} +mod_09_metrics_ui <- function(id) { + tagList( + includeMarkdown("inst/markdown/09_metrics_1.md"), + HTML( + tibble::tribble( + ~Patient, ~Month, ~`Prescription items`, ~`Unique medicines`, + #---------------|-----------|--------------------|----------------- + "A", "April", 4, 2, + "A", "May", 4, 2, + "A", "June", 4, 4, + "A", "July", 6, 3, + "A", "September", 6, 3, + "A", "November", 6, 5, + "B", "April", 13, 10, + "B", "May", 13, 10, + "B", "August", 10, 8, + "C", "October", 4, 4 + ) %>% + knitr::kable("html", caption = "Patient-months example") %>% + kableExtra::kable_styling( + bootstrap_options = c("bordered", "striped", "hover", "condensed", "responsive") + ) + ), + includeMarkdown("inst/markdown/09_metrics_2.md"), + HTML( + tibble::tribble() + ), + includeMarkdown("inst/markdown/09_metrics_3.md"), + HTML( + tibble::tribble() + ), + includeMarkdown("inst/markdown/09_metrics_4.md") + ) +} +``` + +Each markdown file is included in a modules UI function with the `includeMarkdown` function provided by `{shiny}`. You would think there would be an Rmarkdown version of this, but it seems there is not! This simply converts the markdown into HTML. + +```{r} +cat(includeMarkdown("../inst/app/www/assets/markdown/mod_markdown_example.md")) +``` + +## The writing and review cycle + +A common pain point for the team is the process of drafting, reviewing and 'coding' text. There can be many rounds of review and update before a final version of text is agreed. Adding text too early to the app increases the pain, as now the app text and review/draft document (commonly Word with track changes enabled) must be kept in sync. Failing to keep both sources in sync leads to changes being missed and just increases the overhead. At some point however, the text really does need to be added to the app. But there are almost always changes required, despite thinking the version added to the app is the _final_ final version. So there is always going to be some amount of having to juggle multiple sources of text. + +## Standardise and automate + +We can make the process easier and relatively pain-free by standardising our process. This then allows many of the tasks to be automated. The components and procedures of this process include + +1. A 'Word-first' approach, using a set of conventions in how the text is written +2. Automatic conversion between Word and markdown +3. Assisted review of any changes, using file snapshots + +### 'Word-first' approach + +The idea here is to use Word, with tracking of changes, for as long as possible. Add the text to the app only once you are certain that the text is finalised, or you are forced to add it in order to publish the app (e.g. for review by 3rd parties). + +You will need to use a template Word doc in order to use the automation script. This can be generated by knitting `review/styles/draft-styles.Rmd`. You may notice this looks a lot like the standard Rmd template you get when creating a new Rmarkdown document in RStudio, which is exactly what it is! The reason to use this is that the `{officer}` package used in the automation works best with a Word doc that was actually created from Rmd in the first place. There is some scope to redefine some styling if it becomes necessary. + +In addition to using the template document when writing the first draft, there are some conventions that need to be followed on which the script depends. All of the (currently) usable syntax is shown on the _Markdown cheat sheet_ page in the example app. + +1. Use headings starting from 2nd level (Heading 2 in Word); the 1st level heading will be for the app title, which is not set in the markdown +2. Use __bold__ and _italics_ as required +3. Use __single level__ lists, both ordered and bullets +4. Use the `Consolas` font for mono-spaced text such as code +5. Create hyperlinks where you want a link + a. External links - just as you would expect + b. Internal links - construct the URL like `http://127.0.0.1/page?heading` to link to a specific heading of a page, or just `http://127.0.0.1/page` to link to the top of a page + c. Code links - rarely used but can be done by creating a hyperlink with `Consolas` font +6. Don't include extra line breaks (empty lines) between paragraphs or lists +7. Before converting the doc to markdown, make sure to split the text up into chunks according to the file path you want each section to be saved in, using the relative path from the project root folder and starting each filename with a number corresponding to its order, e.g. `inst/app/www/assets/markdown/01_mod_the_first.md` +8. Anything not explicitly mentioned may or may not work; you can try it out and update this guide! + +## Automatic conversion + +### Word to markdown... + +The conversion makes use of the packages [`{officer}`](https://davidgohel.github.io/officer/) and [`{xml2}`](https://xml2.r-lib.org/), along with some common tidyverse packages. + +The general flow of the script is, for each 'chunk' in the document, to + +1. Get plain text on each line +2. Use the [xml](https://en.wikipedia.org/wiki/XML) that underlies the Word doc to create a map of the various styles and hyperlinks +3. Transform the plain text using the style maps, into a a version that uses markdown syntax +4. Write the final text to a markdown file + +### ...markdown to Word + +Conversion from markdown to Word is much easier. The general flow is + +1. Read each line of each file into a list +2. Add the file path to the beginning of each list +3. Combine all lists into one list + +This list is then written as an Rmarkdown document, and finally rendered as a Word doc using the [`rmarkdown::render`](https://pkgs.rstudio.com/rmarkdown/reference/render.html) and [`officedown::rdocx_document`](https://davidgohel.github.io/officedown/reference/rdocx_document.html) functions. The style document specified by `review/styles/draft-styles.Rmd` is also generated and used in the render step. + +## Assisted review + +Once the text is first added to the app, as markdown files, there is probably going to be a time when something needs to change, based on feedback or a rethink of what is written. This review process can be made easy with the following steps + +1. Make sure to base changes on the current state of the app text, by running the `review/md_to_word.R` script first +2. (First review only) To get the initial snapshots, run the first code block ("generate snapshots") in `review/tests/snapshot_tests.R`; each markdown file should have an expectation +3. Finalise the changes in Word, making sure to accept or reject every change +4. Run the second code block ("markdown text is as expected") in `review/tests/snapshot_tests.R` +5. Run the diff tool with `testthat::snapshot_review('snapshot_tests/', "review/tests")` +6. For each file with changes + a. If __all__ changes in a file are as intended, then choose the 'Accept' option + b. If there are unintended changes, fix these in the associated markdown file in `review/temp` (simple copy and paste from the diff viewer works) + c. If you could not 'Accept' a file because some changes were unintended, instead click 'Skip' +7. Once you have finished with the checking and fixing, close the diff viewer +8. Re-run the snapshot tests __BUT MAKE SURE__ to comment out `source("review/scripts/word_to_md.R")` or you will overwrite your amendments and have to redo them! (for extra safety, you could rename the Word doc after step 4, which will make the test error out). You should expect no differences to be found now, but if there are then repeat step 6 to 8 until none are found. +9. Once all is as it should be, you can safely copy the revised markdown files in the `review/temp` folder over the source markdown files for the app in `/inst/app/www/assets/markdown`. __MAKE SURE__ to uncomment `source("review/scripts/word_to_md.R")` in the "markdown text is as expected" code block, if you had to make any fixes, so it is ready for next review cycle. + +The snapshot tests file has extensive comments to follow. + +NOTE: The process outlined could be improved, perhaps by using functions rather than scripts and a test file that requires some manual management of which code to run; hopefully this will get us started and the best process will emerge as we go. From 9a25dbbdf6e5e58d7a5b08d9915d2af98db153c8 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Wed, 1 Nov 2023 08:58:23 +0000 Subject: [PATCH 13/32] Build vignette + trying to fix GHA error --- .Rbuildignore | 2 ++ .gitignore | 2 ++ DESCRIPTION | 1 + vignettes/writing-and-reviewing-text.Rmd | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.Rbuildignore b/.Rbuildignore index 1e2f138..a8e8b43 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -12,3 +12,5 @@ ^\.github$ ^\.lintr$ ^review$ +^doc$ +^Meta$ diff --git a/.gitignore b/.gitignore index 4e53fb2..c885b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ .DS_Store rsconnect .Renviron +/doc/ +/Meta/ diff --git a/DESCRIPTION b/DESCRIPTION index 364b1d3..a4a3daa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,6 +45,7 @@ Suggests: officer, pkgload, purrr, + markdown, rmarkdown, rprojroot, testthat, diff --git a/vignettes/writing-and-reviewing-text.Rmd b/vignettes/writing-and-reviewing-text.Rmd index b5fdf5f..d38fd92 100644 --- a/vignettes/writing-and-reviewing-text.Rmd +++ b/vignettes/writing-and-reviewing-text.Rmd @@ -2,7 +2,7 @@ title: "Writing and reviewing text" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{writing-and-reviewing-text} + %\VignetteIndexEntry{Writing and reviewing text} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- From 0fd3bff517695893395af1b2cdf63f6f27eb548e Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 3 Nov 2023 10:59:27 +0000 Subject: [PATCH 14/32] Rename header args to heading in utils_accessibility + use tagQuery to fix bug in nhs_card_tabstop --- R/mod_chart_example.R | 1 + R/utils_accessibility.R | 71 +++++++++++++++++++++-------------------- man/h1_tabstop.Rd | 4 +-- man/h2_tabstop.Rd | 4 +-- man/h3_tabstop.Rd | 4 +-- man/h4_tabstop.Rd | 4 +-- man/h5_tabstop.Rd | 4 +-- man/h6_tabstop.Rd | 4 +-- man/nhs_card_tabstop.Rd | 4 +-- 9 files changed, 52 insertions(+), 48 deletions(-) diff --git a/R/mod_chart_example.R b/R/mod_chart_example.R index 41f2045..76c3b2b 100644 --- a/R/mod_chart_example.R +++ b/R/mod_chart_example.R @@ -12,6 +12,7 @@ mod_chart_example_ui <- function(id) { h2_tabstop("Second level"), nhs_card_tabstop( heading = "example chart title", + tabindex = 0, nhs_selectInput( inputId = ns("bins"), label = "Number of bins:", diff --git a/R/utils_accessibility.R b/R/utils_accessibility.R index 2cff161..4d08245 100644 --- a/R/utils_accessibility.R +++ b/R/utils_accessibility.R @@ -1,6 +1,6 @@ #' h1_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h1 #' @@ -9,16 +9,16 @@ #' #' @examples #' h1_tabstop("Heading") -h1_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h1_tabstop <- h1(header, ...) %>% +h1_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h1_tabstop <- h1(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' h2_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h2 #' @@ -27,16 +27,16 @@ h1_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' h2_tabstop("Heading") -h2_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h2_tabstop <- h2(header, ...) %>% +h2_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h2_tabstop <- h2(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' h3_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h3 #' @@ -45,16 +45,16 @@ h2_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' h3_tabstop("Heading") -h3_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h3_tabstop <- h3(header, ...) %>% +h3_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h3_tabstop <- h3(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' h4_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h4 #' @@ -63,16 +63,16 @@ h3_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' h4_tabstop("Heading") -h4_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h4_tabstop <- h4(header, ...) %>% +h4_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h4_tabstop <- h4(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' h5_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h5 #' @@ -81,16 +81,16 @@ h4_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' h5_tabstop("Heading") -h5_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h5_tabstop <- h5(header, ...) %>% +h5_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h5_tabstop <- h5(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' h6_tabstop #' -#' @param header Heading text +#' @param heading Heading text #' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h6 #' @@ -99,17 +99,17 @@ h5_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' h6_tabstop("Heading") -h6_tabstop <- function(header, tabindex = 0, ...) { - # create header as typical header plus tabindex attribute - # ensures header will be stopped at when pressing keyboard tab - h6_tabstop <- h6(header, ...) %>% +h6_tabstop <- function(heading, tabindex = 0, ...) { + # create heading as typical heading plus tabindex attribute + # ensures heading will be stopped at when pressing keyboard tab + h6_tabstop <- h6(heading, ...) %>% htmltools::tagAppendAttributes(`tabindex` = tabindex) } #' nhs_card_tabstop #' -#' @param header Card title +#' @param heading Card title #' @param tabindex Number for tabindex, default 0 #' @param ... Card content #' @@ -118,9 +118,12 @@ h6_tabstop <- function(header, tabindex = 0, ...) { #' #' @examples #' nhs_card_tabstop("A card", shiny::p("Some content")) -nhs_card_tabstop <- function(header, tabindex = 0, ...) { +nhs_card_tabstop <- function(heading, tabindex = 0, ...) { # create nhs_card as typical nhs_card plus tabindex attribute # ensures nhs_card will be stopped at when pressing keyboard tab - nhs_card_tabstop <- nhs_card(header, ...) %>% - htmltools::tagAppendAttributes(`tabindex` = tabindex) + htmltools::tagQuery(nhs_card(heading, ...))$ + find(".nhsuk-card__heading")$ + removeAttr("tabindex")$ + addAttr("tabindex" = tabindex)$ + allTags() # Exclude Linting } diff --git a/man/h1_tabstop.Rd b/man/h1_tabstop.Rd index 6a4168b..fe80918 100644 --- a/man/h1_tabstop.Rd +++ b/man/h1_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h1_tabstop} \title{h1_tabstop} \usage{ -h1_tabstop(header, tabindex = 0, ...) +h1_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/h2_tabstop.Rd b/man/h2_tabstop.Rd index ee17103..2740878 100644 --- a/man/h2_tabstop.Rd +++ b/man/h2_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h2_tabstop} \title{h2_tabstop} \usage{ -h2_tabstop(header, tabindex = 0, ...) +h2_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/h3_tabstop.Rd b/man/h3_tabstop.Rd index ce05374..3da32cd 100644 --- a/man/h3_tabstop.Rd +++ b/man/h3_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h3_tabstop} \title{h3_tabstop} \usage{ -h3_tabstop(header, tabindex = 0, ...) +h3_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/h4_tabstop.Rd b/man/h4_tabstop.Rd index 994c88b..5d21bf5 100644 --- a/man/h4_tabstop.Rd +++ b/man/h4_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h4_tabstop} \title{h4_tabstop} \usage{ -h4_tabstop(header, tabindex = 0, ...) +h4_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/h5_tabstop.Rd b/man/h5_tabstop.Rd index df159c7..dd5b061 100644 --- a/man/h5_tabstop.Rd +++ b/man/h5_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h5_tabstop} \title{h5_tabstop} \usage{ -h5_tabstop(header, tabindex = 0, ...) +h5_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/h6_tabstop.Rd b/man/h6_tabstop.Rd index 2dee5cf..0d72ca4 100644 --- a/man/h6_tabstop.Rd +++ b/man/h6_tabstop.Rd @@ -4,10 +4,10 @@ \alias{h6_tabstop} \title{h6_tabstop} \usage{ -h6_tabstop(header, tabindex = 0, ...) +h6_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Heading text} +\item{heading}{Heading text} \item{tabindex}{Number for tabindex, default 0} diff --git a/man/nhs_card_tabstop.Rd b/man/nhs_card_tabstop.Rd index 7095929..e36b822 100644 --- a/man/nhs_card_tabstop.Rd +++ b/man/nhs_card_tabstop.Rd @@ -4,10 +4,10 @@ \alias{nhs_card_tabstop} \title{nhs_card_tabstop} \usage{ -nhs_card_tabstop(header, tabindex = 0, ...) +nhs_card_tabstop(heading, tabindex = 0, ...) } \arguments{ -\item{header}{Card title} +\item{heading}{Card title} \item{tabindex}{Number for tabindex, default 0} From f200a35317cc81cac4ff2fd160b70c01fe7a2ff2 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 3 Nov 2023 11:24:29 +0000 Subject: [PATCH 15/32] Swap ... and tabindex args to allow for easier usage --- R/mod_chart_example.R | 1 - R/utils_accessibility.R | 28 ++++++++++++++-------------- man/h1_tabstop.Rd | 6 +++--- man/h2_tabstop.Rd | 8 ++++---- man/h3_tabstop.Rd | 8 ++++---- man/h4_tabstop.Rd | 8 ++++---- man/h5_tabstop.Rd | 8 ++++---- man/h6_tabstop.Rd | 8 ++++---- man/nhs_card_tabstop.Rd | 6 +++--- 9 files changed, 40 insertions(+), 41 deletions(-) diff --git a/R/mod_chart_example.R b/R/mod_chart_example.R index 76c3b2b..41f2045 100644 --- a/R/mod_chart_example.R +++ b/R/mod_chart_example.R @@ -12,7 +12,6 @@ mod_chart_example_ui <- function(id) { h2_tabstop("Second level"), nhs_card_tabstop( heading = "example chart title", - tabindex = 0, nhs_selectInput( inputId = ns("bins"), label = "Number of bins:", diff --git a/R/utils_accessibility.R b/R/utils_accessibility.R index 4d08245..1d31319 100644 --- a/R/utils_accessibility.R +++ b/R/utils_accessibility.R @@ -1,15 +1,15 @@ #' h1_tabstop #' #' @param heading Heading text -#' @param tabindex Number for tabindex, default 0 #' @inheritDotParams shiny::h1 +#' @param tabindex Number for tabindex, default 0 #' #' @return HTML #' @export #' #' @examples #' h1_tabstop("Heading") -h1_tabstop <- function(heading, tabindex = 0, ...) { +h1_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h1_tabstop <- h1(heading, ...) %>% @@ -19,15 +19,15 @@ h1_tabstop <- function(heading, tabindex = 0, ...) { #' h2_tabstop #' #' @param heading Heading text +#' @inheritDotParams shiny::h1 #' @param tabindex Number for tabindex, default 0 -#' @inheritDotParams shiny::h2 #' #' @return HTML #' @export #' #' @examples #' h2_tabstop("Heading") -h2_tabstop <- function(heading, tabindex = 0, ...) { +h2_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h2_tabstop <- h2(heading, ...) %>% @@ -37,15 +37,15 @@ h2_tabstop <- function(heading, tabindex = 0, ...) { #' h3_tabstop #' #' @param heading Heading text +#' @inheritDotParams shiny::h1 #' @param tabindex Number for tabindex, default 0 -#' @inheritDotParams shiny::h3 #' #' @return HTML #' @export #' #' @examples #' h3_tabstop("Heading") -h3_tabstop <- function(heading, tabindex = 0, ...) { +h3_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h3_tabstop <- h3(heading, ...) %>% @@ -55,15 +55,15 @@ h3_tabstop <- function(heading, tabindex = 0, ...) { #' h4_tabstop #' #' @param heading Heading text +#' @inheritDotParams shiny::h1 #' @param tabindex Number for tabindex, default 0 -#' @inheritDotParams shiny::h4 #' #' @return HTML #' @export #' #' @examples #' h4_tabstop("Heading") -h4_tabstop <- function(heading, tabindex = 0, ...) { +h4_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h4_tabstop <- h4(heading, ...) %>% @@ -73,15 +73,15 @@ h4_tabstop <- function(heading, tabindex = 0, ...) { #' h5_tabstop #' #' @param heading Heading text +#' @inheritDotParams shiny::h1 #' @param tabindex Number for tabindex, default 0 -#' @inheritDotParams shiny::h5 #' #' @return HTML #' @export #' #' @examples #' h5_tabstop("Heading") -h5_tabstop <- function(heading, tabindex = 0, ...) { +h5_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h5_tabstop <- h5(heading, ...) %>% @@ -91,15 +91,15 @@ h5_tabstop <- function(heading, tabindex = 0, ...) { #' h6_tabstop #' #' @param heading Heading text +#' @inheritDotParams shiny::h1 #' @param tabindex Number for tabindex, default 0 -#' @inheritDotParams shiny::h6 #' #' @return HTML #' @export #' #' @examples #' h6_tabstop("Heading") -h6_tabstop <- function(heading, tabindex = 0, ...) { +h6_tabstop <- function(heading, ..., tabindex = 0) { # create heading as typical heading plus tabindex attribute # ensures heading will be stopped at when pressing keyboard tab h6_tabstop <- h6(heading, ...) %>% @@ -110,15 +110,15 @@ h6_tabstop <- function(heading, tabindex = 0, ...) { #' nhs_card_tabstop #' #' @param heading Card title -#' @param tabindex Number for tabindex, default 0 #' @param ... Card content +#' @param tabindex Number for tabindex, default 0 #' #' @return HTML #' @export #' #' @examples #' nhs_card_tabstop("A card", shiny::p("Some content")) -nhs_card_tabstop <- function(heading, tabindex = 0, ...) { +nhs_card_tabstop <- function(heading, ..., tabindex = 0) { # create nhs_card as typical nhs_card plus tabindex attribute # ensures nhs_card will be stopped at when pressing keyboard tab htmltools::tagQuery(nhs_card(heading, ...))$ diff --git a/man/h1_tabstop.Rd b/man/h1_tabstop.Rd index fe80918..113e5e9 100644 --- a/man/h1_tabstop.Rd +++ b/man/h1_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h1_tabstop} \title{h1_tabstop} \usage{ -h1_tabstop(heading, tabindex = 0, ...) +h1_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/h2_tabstop.Rd b/man/h2_tabstop.Rd index 2740878..269ea23 100644 --- a/man/h2_tabstop.Rd +++ b/man/h2_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h2_tabstop} \title{h2_tabstop} \usage{ -h2_tabstop(heading, tabindex = 0, ...) +h2_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ - Arguments passed on to \code{\link[shiny:reexports]{shiny::h2}} + Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/h3_tabstop.Rd b/man/h3_tabstop.Rd index 3da32cd..aeb7920 100644 --- a/man/h3_tabstop.Rd +++ b/man/h3_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h3_tabstop} \title{h3_tabstop} \usage{ -h3_tabstop(heading, tabindex = 0, ...) +h3_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ - Arguments passed on to \code{\link[shiny:reexports]{shiny::h3}} + Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/h4_tabstop.Rd b/man/h4_tabstop.Rd index 5d21bf5..1faa472 100644 --- a/man/h4_tabstop.Rd +++ b/man/h4_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h4_tabstop} \title{h4_tabstop} \usage{ -h4_tabstop(heading, tabindex = 0, ...) +h4_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ - Arguments passed on to \code{\link[shiny:reexports]{shiny::h4}} + Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/h5_tabstop.Rd b/man/h5_tabstop.Rd index dd5b061..6460307 100644 --- a/man/h5_tabstop.Rd +++ b/man/h5_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h5_tabstop} \title{h5_tabstop} \usage{ -h5_tabstop(heading, tabindex = 0, ...) +h5_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ - Arguments passed on to \code{\link[shiny:reexports]{shiny::h5}} + Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/h6_tabstop.Rd b/man/h6_tabstop.Rd index 0d72ca4..1a7d1a2 100644 --- a/man/h6_tabstop.Rd +++ b/man/h6_tabstop.Rd @@ -4,18 +4,18 @@ \alias{h6_tabstop} \title{h6_tabstop} \usage{ -h6_tabstop(heading, tabindex = 0, ...) +h6_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Heading text} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{ - Arguments passed on to \code{\link[shiny:reexports]{shiny::h6}} + Arguments passed on to \code{\link[shiny:reexports]{shiny::h1}} \describe{ \item{\code{}}{} }} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML diff --git a/man/nhs_card_tabstop.Rd b/man/nhs_card_tabstop.Rd index e36b822..5f6d9a8 100644 --- a/man/nhs_card_tabstop.Rd +++ b/man/nhs_card_tabstop.Rd @@ -4,14 +4,14 @@ \alias{nhs_card_tabstop} \title{nhs_card_tabstop} \usage{ -nhs_card_tabstop(heading, tabindex = 0, ...) +nhs_card_tabstop(heading, ..., tabindex = 0) } \arguments{ \item{heading}{Card title} -\item{tabindex}{Number for tabindex, default 0} - \item{...}{Card content} + +\item{tabindex}{Number for tabindex, default 0} } \value{ HTML From 778da876b967a56663ba8e5ff4c8c31d4a3635bb Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 6 Nov 2023 15:47:19 +0000 Subject: [PATCH 16/32] Add H1 + create temp folder if needed + add section markers in test file --- .../www/assets/markdown/mod_markdown_example.md | 4 +++- review/scripts/word_to_md.R | 3 +++ review/tests/snapshot_tests.R | 15 +++++++++++++-- vignettes/writing-and-reviewing-text.Rmd | 14 +++++++------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/inst/app/www/assets/markdown/mod_markdown_example.md b/inst/app/www/assets/markdown/mod_markdown_example.md index 577178f..2a8a970 100644 --- a/inst/app/www/assets/markdown/mod_markdown_example.md +++ b/inst/app/www/assets/markdown/mod_markdown_example.md @@ -6,14 +6,16 @@ The markdown syntax compatible with the review automation scripts is shown below ### Heading -Headings from level two to four can be used. First level heading is reserved for the main title, so should never appear in markdown files. +Headings from level one to four can be used. ``` +# H1 ## H2 ### H3 #### H4 ``` +# H1 ## H2 ### H3 #### H4 diff --git a/review/scripts/word_to_md.R b/review/scripts/word_to_md.R index 9b8e134..051d6eb 100644 --- a/review/scripts/word_to_md.R +++ b/review/scripts/word_to_md.R @@ -410,6 +410,8 @@ pwalk( ), text = case_match( style_name, + "Heading 1" ~ paste0("# ", text), + "heading 1" ~ paste0("# ", text), "Heading 2" ~ paste0("## ", text), "heading 2" ~ paste0("## ", text), "Heading 3" ~ paste0("### ", text), @@ -438,6 +440,7 @@ pwalk( walk(needs_blank_after, \(x) doc_df <<- add_row(doc_df, text = "", .after = x)) # Write the markdown file + dir.create(md_out_dir, showWarnings = FALSE) writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) } ) diff --git a/review/tests/snapshot_tests.R b/review/tests/snapshot_tests.R index 7b369fb..30c21c5 100644 --- a/review/tests/snapshot_tests.R +++ b/review/tests/snapshot_tests.R @@ -4,7 +4,8 @@ library(rprojroot) local_edition(3) -# INSTRUCTIONS + +# Instructions ------------------------------------------------------------ # 1. Get initial snapshots # 2. Comment out the first code block # 3. Uncomment the rest of the code @@ -29,6 +30,8 @@ local_edition(3) # due to reversions, uncomment it now so it is ready for the next set of # changes. + +# Generate snapshots ------------------------------------------------------ # 1. To get the initial snapshot, run the below block, with an expectation for # each reference file. Use 'Run Tests' button above right. # You are basically saying "all these files should look as they are now in @@ -46,8 +49,10 @@ test_that("generate snapshots", { # 3. Uncomment the below block, ready for future changes + +# Compare markdown -------------------------------------------------------- # 4. When you make changes to the files, use 'Run Tests' button above right -# test_that("markdown text is as expected", { +# test_that("compare markdown", { # with_dir(find_package_root_file(), { # source("review/scripts/word_to_md.R") # @@ -56,11 +61,15 @@ test_that("generate snapshots", { # }) # }) + +# Run diff viewer --------------------------------------------------------- # 5. If any differences are present (there should be if you changed the files...) # a diff viewer can be opened by running the code below in the console (you can # open it in browser using the 'Show in new window' button in the Viewer pane) # testthat::snapshot_review('snapshot_tests/', "review/tests") + +# Review - accept or fix -------------------------------------------------- # 6. Review the changes in each file one by one. # If ALL changes in a file are as intended, then choose the 'Accept' option. # @@ -78,6 +87,8 @@ test_that("generate snapshots", { # differences to be found now, but if there are then repeat step 6 to 8 until # none are found. + +# Replace markdown -------------------------------------------------------- # 9. It is now safe to copy the revised markdown files from the review/temp folder # over the app markdown files in inst/app/www/assets/markdown. # diff --git a/vignettes/writing-and-reviewing-text.Rmd b/vignettes/writing-and-reviewing-text.Rmd index d38fd92..e77b780 100644 --- a/vignettes/writing-and-reviewing-text.Rmd +++ b/vignettes/writing-and-reviewing-text.Rmd @@ -93,7 +93,7 @@ You will need to use a template Word doc in order to use the automation script. In addition to using the template document when writing the first draft, there are some conventions that need to be followed on which the script depends. All of the (currently) usable syntax is shown on the _Markdown cheat sheet_ page in the example app. -1. Use headings starting from 2nd level (Heading 2 in Word); the 1st level heading will be for the app title, which is not set in the markdown +1. Use headings starting from 1st to 4th level (Heading n, in Word) 2. Use __bold__ and _italics_ as required 3. Use __single level__ lists, both ordered and bullets 4. Use the `Consolas` font for mono-spaced text such as code @@ -133,17 +133,17 @@ This list is then written as an Rmarkdown document, and finally rendered as a Wo Once the text is first added to the app, as markdown files, there is probably going to be a time when something needs to change, based on feedback or a rethink of what is written. This review process can be made easy with the following steps 1. Make sure to base changes on the current state of the app text, by running the `review/md_to_word.R` script first -2. (First review only) To get the initial snapshots, run the first code block ("generate snapshots") in `review/tests/snapshot_tests.R`; each markdown file should have an expectation +2. (First review only) To get the initial snapshots, run the first code block (`Generate snapshots`) in `review/tests/snapshot_tests.R`; each markdown file should have an expectation 3. Finalise the changes in Word, making sure to accept or reject every change -4. Run the second code block ("markdown text is as expected") in `review/tests/snapshot_tests.R` -5. Run the diff tool with `testthat::snapshot_review('snapshot_tests/', "review/tests")` -6. For each file with changes +4. Run the second code block (`Compare markdown`) in `review/tests/snapshot_tests.R` +5. Run the diff tool `Run diff viewer` +6. For each file with changes (`Review - accept or fix`) a. If __all__ changes in a file are as intended, then choose the 'Accept' option b. If there are unintended changes, fix these in the associated markdown file in `review/temp` (simple copy and paste from the diff viewer works) c. If you could not 'Accept' a file because some changes were unintended, instead click 'Skip' -7. Once you have finished with the checking and fixing, close the diff viewer +7. Once you have finished with the checking and fixing, close the diff viewer. 8. Re-run the snapshot tests __BUT MAKE SURE__ to comment out `source("review/scripts/word_to_md.R")` or you will overwrite your amendments and have to redo them! (for extra safety, you could rename the Word doc after step 4, which will make the test error out). You should expect no differences to be found now, but if there are then repeat step 6 to 8 until none are found. -9. Once all is as it should be, you can safely copy the revised markdown files in the `review/temp` folder over the source markdown files for the app in `/inst/app/www/assets/markdown`. __MAKE SURE__ to uncomment `source("review/scripts/word_to_md.R")` in the "markdown text is as expected" code block, if you had to make any fixes, so it is ready for next review cycle. +9. Once all is as it should be, you can safely copy the revised markdown files in the `review/temp` folder over the source markdown files for the app in `/inst/app/www/assets/markdown`. __MAKE SURE__ to uncomment `source("review/scripts/word_to_md.R")` in the "markdown text is as expected" code block, if you had to make any fixes, so it is ready for next review cycle (`Replace markdown`). The snapshot tests file has extensive comments to follow. From 9f75fa9de0fc4f2a0e5da9d6a9ed026f925a4d7d Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Wed, 15 Nov 2023 09:09:01 +0000 Subject: [PATCH 17/32] WIP --- .Rbuildignore | 1 - DESCRIPTION | 4 +- NAMESPACE | 1 + R/utils-review.R | 178 ++++++++++++++++++ inst/review/review.docx | Bin 0 -> 16090 bytes {review => inst/review}/scripts/md_to_word.R | 0 {review => inst/review}/scripts/word_to_md.R | 0 .../review}/styles/draft-styles.rmd | 0 .../review}/tests/snapshot_tests.R | 0 man/md_to_word.Rd | 43 +++++ tests/testthat.R | 12 ++ tests/testthat/test-utils-review.R | 87 +++++++++ 12 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 R/utils-review.R create mode 100644 inst/review/review.docx rename {review => inst/review}/scripts/md_to_word.R (100%) rename {review => inst/review}/scripts/word_to_md.R (100%) rename {review => inst/review}/styles/draft-styles.rmd (100%) rename {review => inst/review}/tests/snapshot_tests.R (100%) create mode 100644 man/md_to_word.Rd create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-utils-review.R diff --git a/.Rbuildignore b/.Rbuildignore index a8e8b43..b08a772 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,6 +11,5 @@ ^gitleaks.toml$ ^\.github$ ^\.lintr$ -^review$ ^doc$ ^Meta$ diff --git a/DESCRIPTION b/DESCRIPTION index a4a3daa..b1ebb64 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -48,9 +48,10 @@ Suggests: markdown, rmarkdown, rprojroot, - testthat, + testthat (>= 3.0.0), tidyr, usethis, + waldo, withr, xml2 Remotes: @@ -60,3 +61,4 @@ Encoding: UTF-8 LazyData: true RoxygenNote: 7.2.3 VignetteBuilder: knitr +Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index d097394..32ee157 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,7 @@ export(h3_tabstop) export(h4_tabstop) export(h5_tabstop) export(h6_tabstop) +export(md_to_word) export(nhs_card) export(nhs_card_tabstop) export(nhs_footer) diff --git a/R/utils-review.R b/R/utils-review.R new file mode 100644 index 0000000..a0fc446 --- /dev/null +++ b/R/utils-review.R @@ -0,0 +1,178 @@ +# library(testthat) +# library(withr) +# library(rprojroot) +# +# local_edition(3) + + +#' Generate Word doc from markdown files +#' +#' All markdown files in a folder can be used to generate a Word document. The +#' main use case is for use in reviewing often changing text when writing final +#' output of initiatives. It can be used standalone for adhoc purposes. +#' +#' @param md_dir Markdown directory +#' @param rv_dir Review directory +#' @param docx_file Output Word document file name +#' @param styles_rmd Path to Rmarkdown that creates Word template +#' +#' @return Path of generated Word doc +#' @export +#' +#' @examples +#' # For standard use case, default args are fine +#' md_to_word() +#' +#' # May be times when you want to use outside of the usual use case, e.g. +#' # generate Word doc for adhoc purposes +#' md_to_word( +#' "my/adhoc/markdown", +#' "C:/Users/CYPHER/Downloads", +#' "adhoc.docx", +#' system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") +#' ) +md_to_word <- function(md_dir = "inst/app/www/assets/markdown", rv_dir = "inst/review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd") { + + styles_doc <- rmarkdown::render( + styles_rmd, + output_dir = tempdir(), + output_file = tempfile(), + quiet = TRUE + ) + + # Get paths of md files + md_files <- Sys.glob(file.path(md_dir, "*.md")) + + # Combine them into one rmarkdown file + all_md <- purrr::map(md_files, readLines) # List of md content + + # Keep only parent folder and file name + md_files <- file.path( + rev(strsplit(dirname(md_files), "/")[[1]])[[1]], + basename(md_files) + ) + + all_md <- purrr::map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add file marker + all_md <- purrr::reduce(all_md, c) # All content in one vector + writeLines(all_md, file.path(tempdir(), "all_md.rmd")) + + # Create Word document + rmarkdown::render( + file.path(tempdir(), "all_md.rmd"), + officedown::rdocx_document( + reference_docx = styles_doc, + toc = FALSE, + number_sections = FALSE + ), + output_dir = rv_dir, + output_file = docx_file, + quiet = TRUE + ) + + # Return path of generated Word doc + file.path(rv_dir, docx_file) +} + + + +# Instructions ------------------------------------------------------------ +# 1. Get initial snapshots +# 2. Comment out the first code block +# 3. Uncomment the rest of the code +# 4. Whenever you make changes to these files, 'Run Tests' again +# 5. Any differences will be caught; run the below code in the console to review +# them in a shiny app (working dir is project root): +# testthat::snapshot_review('snapshot_tests/', "review/tests") +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# If there are some changes that were not intended, you should fix these in +# the temporary markdown files (review/temp). You can leave the diff viewer +# open and revert the changes directly by a simple copy and paste of the +# original content. Click 'Skip' once the reversions are complete. +# 7. Close the diff viewer once only skipped files remain. +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. +# 9. If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, uncomment it now so it is ready for the next set of +# changes. + +# gen_snaps <- function() { +# withr::with_dir(rprojroot::find_package_root_file(), { +# source("review/scripts/md_to_word.R") +# +# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") +# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") +# }) +# } + +# Generate snapshots ------------------------------------------------------ +# 1. To get the initial snapshot, run the below block, with an expectation for +# each reference file. Use 'Run Tests' button above right. +# You are basically saying "all these files should look as they are now in +# future". +# test_that("generate snapshots", { +# with_dir(find_package_root_file(), { +# source("review/scripts/md_to_word.R") +# +# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") +# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") +# }) +# }) + +# 2. Comment the above block out now + +# 3. Uncomment the below block, ready for future changes + + +# Compare markdown -------------------------------------------------------- +# 4. When you make changes to the files, use 'Run Tests' button above right +# test_that("compare markdown", { +# with_dir(find_package_root_file(), { +# source("review/scripts/word_to_md.R") +# +# expect_snapshot_file("review/temp/01_mod_the_first.md") +# expect_snapshot_file("review/temp/02_mod_the_second.md") +# }) +# }) + + +# Run diff viewer --------------------------------------------------------- +# 5. If any differences are present (there should be if you changed the files...) +# a diff viewer can be opened by running the code below in the console (you can +# open it in browser using the 'Show in new window' button in the Viewer pane) +# testthat::snapshot_review('snapshot_tests/', "review/tests") + + +# Review - accept or fix -------------------------------------------------- +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# +# If there are some changes that were not intended, you should fix these in +# the source files. You can leave the diff viewer open and revert the changes +# directly by a simple copy and paste of the original content. +# +# Click 'Skip' once the reversions are complete. + +# 7. Close the diff viewer once only skipped files remain. + +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. + + +# Replace markdown -------------------------------------------------------- +# 9. It is now safe to copy the revised markdown files from the review/temp folder +# over the app markdown files in inst/app/www/assets/markdown. +# +# If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, MAKE SURE to uncomment it now so it is ready for the next +# set of changes. \ No newline at end of file diff --git a/inst/review/review.docx b/inst/review/review.docx new file mode 100644 index 0000000000000000000000000000000000000000..97449d41d7cdeb69367aa96447b5b62338df35de GIT binary patch literal 16090 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 literal 0 HcmV?d00001 diff --git a/review/scripts/md_to_word.R b/inst/review/scripts/md_to_word.R similarity index 100% rename from review/scripts/md_to_word.R rename to inst/review/scripts/md_to_word.R diff --git a/review/scripts/word_to_md.R b/inst/review/scripts/word_to_md.R similarity index 100% rename from review/scripts/word_to_md.R rename to inst/review/scripts/word_to_md.R diff --git a/review/styles/draft-styles.rmd b/inst/review/styles/draft-styles.rmd similarity index 100% rename from review/styles/draft-styles.rmd rename to inst/review/styles/draft-styles.rmd diff --git a/review/tests/snapshot_tests.R b/inst/review/tests/snapshot_tests.R similarity index 100% rename from review/tests/snapshot_tests.R rename to inst/review/tests/snapshot_tests.R diff --git a/man/md_to_word.Rd b/man/md_to_word.Rd new file mode 100644 index 0000000..bdbaa30 --- /dev/null +++ b/man/md_to_word.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-review.R +\name{md_to_word} +\alias{md_to_word} +\title{Generate Word doc from markdown files} +\usage{ +md_to_word( + md_dir = "inst/app/www/assets/markdown", + rv_dir = "review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd" +) +} +\arguments{ +\item{md_dir}{Markdown directory} + +\item{rv_dir}{Review directory} + +\item{docx_file}{Output Word document file name} + +\item{styles_rmd}{Path to Rmarkdown that creates Word template} +} +\value{ +Path of generated Word doc +} +\description{ +All markdown files in a folder can be used to generate a Word document. The +main use case is for use in reviewing often changing text when writing final +output of initiatives. It can be used standalone for adhoc purposes. +} +\examples{ +# For standard use case, default args are fine +md_to_word() + +# May be times when you want to use outside of the usual use case, e.g. +# generate Word doc for adhoc purposes +md_to_word( + "my/adhoc/markdown", + "C:/Users/CYPHER/Downloads", + "adhoc.docx", + system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") +) +} diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..6a67fca --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(nhsbsaShinyR) + +test_check("nhsbsaShinyR") diff --git a/tests/testthat/test-utils-review.R b/tests/testthat/test-utils-review.R new file mode 100644 index 0000000..6219667 --- /dev/null +++ b/tests/testthat/test-utils-review.R @@ -0,0 +1,87 @@ +test_that("md_to_word generates expected Word doc", { + throwaway <- withr::local_tempdir(pattern = "throwaway") + + md_dir <- file.path(throwaway, "markdown") + dir.create(md_dir) + + md_lines <- c( + "# H1 ", + "## H2 ", + "### H3 ", + "#### H4 ", + "__bold text__ ", + "_italicized text_ ", + "1. First item ", + "2. Second item ", + "3. Third item ", + "- First item ", + "- Second item ", + "- Third item ", + "Here is some inline ````code````. ", + "[Markdown Guide](https://www.markdownguide.org) ", + "[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) " + ) + + md_file1 <- writeLines( + md_lines, + file.path(md_dir, "01_md_file.md") + ) + + md_file2 <- writeLines( + md_lines, + file.path(md_dir, "02_md_file.md") + ) + + rv_dir <- file.path(throwaway, "review") + + docx_file <- "review.docx" + + styles_rmd <- system.file( + "review", "styles", "draft-styles.rmd", + package = "nhsbsaShinyR" + ) + + docx_path <- md_to_word( + md_dir = md_dir, + rv_dir = rv_dir, + docx_file = docx_file, + styles_rmd = styles_rmd + ) + + docx_path <- file.path(rv_dir, docx_file) + + # Need to use same created/modified date each run, or output doc will not + # match snapshot + docx <- officer::read_docx(docx_path) + + props_file <- file.path( + docx$package_dir, + "docProps", + "core.xml" + ) + props_xml <- xml2::read_xml(props_file) + props_xml <- xml2::xml_find_all( + props_xml, + "//*[starts-with(name(), 'dcterms')]" + ) + xml2::xml_text(props_xml) <- c("2023-11-07T16:07:06Z") + + xml2::write_xml( + xml2::xml_root(props_xml), + file.path( + docx$package_dir, + "docProps", + "core.xml" + ) + ) + + tryCatch( + expect_snapshot_file(docx_path), + error = \(e) { + cat("\n\n", waldo::compare( + officer::read_docx("_snaps/utils-review/review.docx"), + officer::read_docx("_snaps/utils-review/review.new.docx") + ), sep = "\n") + } + ) +}) From 0f12c8d5651adea4f69b62b61062b2dfd3bce81e Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 17 Nov 2023 13:43:32 +0000 Subject: [PATCH 18/32] Pull review scripts into functions, with tests --- DESCRIPTION | 22 +- NAMESPACE | 1 + R/test-helpers.R | 65 ++ R/utils-review.R | 178 ----- R/utils_review.R | 665 ++++++++++++++++++ inst/review/scripts/md_to_word.R | 54 -- inst/review/scripts/word_to_md.R | 446 ------------ man/md_to_word.Rd | 7 +- man/word_to_md.Rd | 46 ++ tests/testthat/_snaps/utils_review.md | 128 ++++ .../_snaps/utils_review/01_md_file.md | 29 + .../_snaps/utils_review/02_md_file.md | 29 + {inst/review => tests/testthat}/review.docx | Bin 16090 -> 13523 bytes tests/testthat/test-utils-review.R | 87 --- tests/testthat/test-utils_review.R | 78 ++ 15 files changed, 1056 insertions(+), 779 deletions(-) create mode 100644 R/test-helpers.R delete mode 100644 R/utils-review.R create mode 100644 R/utils_review.R delete mode 100644 inst/review/scripts/md_to_word.R delete mode 100644 inst/review/scripts/word_to_md.R create mode 100644 man/word_to_md.Rd create mode 100644 tests/testthat/_snaps/utils_review.md create mode 100644 tests/testthat/_snaps/utils_review/01_md_file.md create mode 100644 tests/testthat/_snaps/utils_review/02_md_file.md rename {inst/review => tests/testthat}/review.docx (53%) delete mode 100644 tests/testthat/test-utils-review.R create mode 100644 tests/testthat/test-utils_review.R diff --git a/DESCRIPTION b/DESCRIPTION index b1ebb64..93290e8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: nhsbsaShinyR Title: Template for NHSBSA {shiny} apps -Version: 0.0.0.9003 +Version: 0.0.0.9004 Authors@R: c(person(given = "DALL", family = "", @@ -35,25 +35,25 @@ Imports: htmltools, magrittr, nhsbsaR, + officedown, + officer, + purrr, rlang, + rmarkdown, scrollytell, shiny, - shinyjs + shinyjs, + tidyr, + utils, + withr, + xml2 Suggests: knitr, - officedown, - officer, pkgload, - purrr, markdown, - rmarkdown, - rprojroot, testthat (>= 3.0.0), - tidyr, usethis, - waldo, - withr, - xml2 + waldo Remotes: nhsbsa-data-analytics/nhsbsaR, statistiekcbs/scrollytell diff --git a/NAMESPACE b/NAMESPACE index 32ee157..45764f3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,6 +19,7 @@ export(nhs_navlistPanel) export(nhs_selectInput) export(nhs_selectizeInput) export(run_app) +export(word_to_md) import(shiny) importFrom(golem,activate_js) importFrom(golem,add_resource_path) diff --git a/R/test-helpers.R b/R/test-helpers.R new file mode 100644 index 0000000..e0c9923 --- /dev/null +++ b/R/test-helpers.R @@ -0,0 +1,65 @@ +local_create_md <- function(temp_dir = tempdir(), env = parent.frame()) { + md_dir <- file.path(temp_dir, "markdown") + dir.create(md_dir, showWarnings = FALSE) + withr::defer(unlink(temp_dir), envir = env) + + md_lines <- c( + "# H1", + "## H2", + "### H3", + "#### H4", + "__bold text__", + "", + "_italicized text_", + "", + "Numbered list", + "", + "1. First item", + "2. Second item", + "3. Third item", + "", + "Bulleted list", + "", + "- First item", + "- Second item", + "- Third item", + "", + "Here is some inline ````code````.", + "", + "[Markdown Guide](https://www.markdownguide.org)", + "", + "[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR)" + ) + + writeLines( + md_lines, + file.path(md_dir, "01_md_file.md") + ) + + writeLines( + md_lines, + file.path(md_dir, "02_md_file.md") + ) + + md_dir +} + + +local_create_word_doc <- function(temp_dir = tempdir(), md_dir, rv_dir, docx_file, + env = parent.frame()) { + withr::defer(unlink(temp_dir), envir = env) + + styles_rmd <- system.file( + "review", "styles", "draft-styles.rmd", + package = "nhsbsaShinyR" + ) + + docx_path <- md_to_word( + md_dir = md_dir, + rv_dir = rv_dir, + docx_file = docx_file, + styles_rmd = styles_rmd + ) + + docx_path +} diff --git a/R/utils-review.R b/R/utils-review.R deleted file mode 100644 index a0fc446..0000000 --- a/R/utils-review.R +++ /dev/null @@ -1,178 +0,0 @@ -# library(testthat) -# library(withr) -# library(rprojroot) -# -# local_edition(3) - - -#' Generate Word doc from markdown files -#' -#' All markdown files in a folder can be used to generate a Word document. The -#' main use case is for use in reviewing often changing text when writing final -#' output of initiatives. It can be used standalone for adhoc purposes. -#' -#' @param md_dir Markdown directory -#' @param rv_dir Review directory -#' @param docx_file Output Word document file name -#' @param styles_rmd Path to Rmarkdown that creates Word template -#' -#' @return Path of generated Word doc -#' @export -#' -#' @examples -#' # For standard use case, default args are fine -#' md_to_word() -#' -#' # May be times when you want to use outside of the usual use case, e.g. -#' # generate Word doc for adhoc purposes -#' md_to_word( -#' "my/adhoc/markdown", -#' "C:/Users/CYPHER/Downloads", -#' "adhoc.docx", -#' system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") -#' ) -md_to_word <- function(md_dir = "inst/app/www/assets/markdown", rv_dir = "inst/review", - docx_file = "review.docx", - styles_rmd = "inst/review/styles/draft-styles.rmd") { - - styles_doc <- rmarkdown::render( - styles_rmd, - output_dir = tempdir(), - output_file = tempfile(), - quiet = TRUE - ) - - # Get paths of md files - md_files <- Sys.glob(file.path(md_dir, "*.md")) - - # Combine them into one rmarkdown file - all_md <- purrr::map(md_files, readLines) # List of md content - - # Keep only parent folder and file name - md_files <- file.path( - rev(strsplit(dirname(md_files), "/")[[1]])[[1]], - basename(md_files) - ) - - all_md <- purrr::map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add file marker - all_md <- purrr::reduce(all_md, c) # All content in one vector - writeLines(all_md, file.path(tempdir(), "all_md.rmd")) - - # Create Word document - rmarkdown::render( - file.path(tempdir(), "all_md.rmd"), - officedown::rdocx_document( - reference_docx = styles_doc, - toc = FALSE, - number_sections = FALSE - ), - output_dir = rv_dir, - output_file = docx_file, - quiet = TRUE - ) - - # Return path of generated Word doc - file.path(rv_dir, docx_file) -} - - - -# Instructions ------------------------------------------------------------ -# 1. Get initial snapshots -# 2. Comment out the first code block -# 3. Uncomment the rest of the code -# 4. Whenever you make changes to these files, 'Run Tests' again -# 5. Any differences will be caught; run the below code in the console to review -# them in a shiny app (working dir is project root): -# testthat::snapshot_review('snapshot_tests/', "review/tests") -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# If there are some changes that were not intended, you should fix these in -# the temporary markdown files (review/temp). You can leave the diff viewer -# open and revert the changes directly by a simple copy and paste of the -# original content. Click 'Skip' once the reversions are complete. -# 7. Close the diff viewer once only skipped files remain. -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. -# 9. If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, uncomment it now so it is ready for the next set of -# changes. - -# gen_snaps <- function() { -# withr::with_dir(rprojroot::find_package_root_file(), { -# source("review/scripts/md_to_word.R") -# -# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") -# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") -# }) -# } - -# Generate snapshots ------------------------------------------------------ -# 1. To get the initial snapshot, run the below block, with an expectation for -# each reference file. Use 'Run Tests' button above right. -# You are basically saying "all these files should look as they are now in -# future". -# test_that("generate snapshots", { -# with_dir(find_package_root_file(), { -# source("review/scripts/md_to_word.R") -# -# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") -# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") -# }) -# }) - -# 2. Comment the above block out now - -# 3. Uncomment the below block, ready for future changes - - -# Compare markdown -------------------------------------------------------- -# 4. When you make changes to the files, use 'Run Tests' button above right -# test_that("compare markdown", { -# with_dir(find_package_root_file(), { -# source("review/scripts/word_to_md.R") -# -# expect_snapshot_file("review/temp/01_mod_the_first.md") -# expect_snapshot_file("review/temp/02_mod_the_second.md") -# }) -# }) - - -# Run diff viewer --------------------------------------------------------- -# 5. If any differences are present (there should be if you changed the files...) -# a diff viewer can be opened by running the code below in the console (you can -# open it in browser using the 'Show in new window' button in the Viewer pane) -# testthat::snapshot_review('snapshot_tests/', "review/tests") - - -# Review - accept or fix -------------------------------------------------- -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# -# If there are some changes that were not intended, you should fix these in -# the source files. You can leave the diff viewer open and revert the changes -# directly by a simple copy and paste of the original content. -# -# Click 'Skip' once the reversions are complete. - -# 7. Close the diff viewer once only skipped files remain. - -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. - - -# Replace markdown -------------------------------------------------------- -# 9. It is now safe to copy the revised markdown files from the review/temp folder -# over the app markdown files in inst/app/www/assets/markdown. -# -# If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, MAKE SURE to uncomment it now so it is ready for the next -# set of changes. \ No newline at end of file diff --git a/R/utils_review.R b/R/utils_review.R new file mode 100644 index 0000000..8901818 --- /dev/null +++ b/R/utils_review.R @@ -0,0 +1,665 @@ +#' Generate Word doc from markdown files +#' +#' All markdown files in a folder can be used to generate a Word document. The +#' main use case is for use in reviewing often changing text when writing final +#' output of initiatives. It can be used standalone for adhoc purposes. +#' +#' @param md_dir Markdown directory +#' @param rv_dir Review directory +#' @param docx_file Output Word document file name +#' @param styles_rmd Path to Rmarkdown that creates Word template +#' +#' @return Path of generated Word doc +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' md_to_word() +#' +#' # May be times when you want to use outside of the usual use case, e.g. +#' # generate Word doc for adhoc purposes +#' md_to_word( +#' "my/adhoc/markdown", +#' "C:/Users/CYPHER/Downloads", +#' "adhoc.docx", +#' system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") +#' )} +md_to_word <- function(md_dir = "inst/app/www/assets/markdown", + rv_dir = "inst/review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd") { + styles_doc <- rmarkdown::render( + styles_rmd, + output_dir = tempdir(), + output_file = tempfile(), + quiet = TRUE + ) + + # Get paths of md files + md_files <- Sys.glob(file.path(md_dir, "*.md")) + + # Combine them into one rmarkdown file + all_md <- purrr::map(md_files, readLines) # List of md content + + # Keep only parent folder and file name + md_files <- file.path( + rev(strsplit(dirname(md_files), "/")[[1]])[[1]], + basename(md_files) + ) + + all_md <- purrr::map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add file marker + all_md <- purrr::reduce(all_md, c) # All content in one vector + writeLines(all_md, file.path(tempdir(), "all_md.rmd")) + + # Create Word document + rmarkdown::render( + file.path(tempdir(), "all_md.rmd"), + officedown::rdocx_document( + reference_docx = styles_doc, + toc = FALSE, + number_sections = FALSE + ), + output_dir = rv_dir, + output_file = docx_file, + quiet = TRUE + ) + + # Return path of generated Word doc + file.path(rv_dir, docx_file) +} + + +#' Create a map of specific styles in a Word document +#' +#' The arguments provided act as filters and conditions. As the document XML is +#' parsed, elements not filtered out are those matching the desired style. +#' +#' @param doc An `officer` `docx` object +#' @param t1 First tag name +#' @param t2 Second tag name +#' @param t3 Third tag name +#' @param val3 Value of `val` attribute for third tag +#' @param t4 Fourth tag name +#' @param val4 Value of `val` attribute for fourth tag +#' +#' @return A named list of lists. Names correspond to lines in the document. The +#' sub-lists contain the character indexes of the text with specific style. +#' There can be multiple entries for a single line. +#' +#' @noRd +#' +#' @examples +#' doc <- officer::read_docx( +#' system.file( +#' "tests", "testthat", "review.docx", +#' package = "nhsbsaShinyR" +#' ) +#' ) +#' style_map(doc, "r", "rPr", "b") # bold text +#' style_map(doc, "r", "rPr", "i") # italics text +#' style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "Hyperlink") # hyperlinks +#' style_map(doc, "r", "rPr", "rStyle", "VerbatimChar") # monospace code +#' style_map( +#' doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar" +#' ) # monospace code hyperlinks +style_map <- function(doc, t1, t2 = NULL, + t3 = NULL, val3 = NULL, + t4 = NULL, val4 = NULL) { + smap <- list() + num_blank <- 0 + + for (i in 1:length(doc)) { + doc$officer_cursor$which <- i + num_chars <- 0 + parent <- officer::docx_current_block_xml(doc) + first_children <- xml2::xml_children(parent) + if(xml2::xml_text(parent) == "") num_blank <- num_blank + 1 + for (first_child in first_children) { + match_found <- FALSE + num_chars <- num_chars + nchar(xml2::xml_text(first_child)) + if (!xml2::xml_name(first_child) == t1) next + if (!is.null(t2)) { + second_children <- xml2::xml_children(first_child) + for (second_child in second_children) { + if (!xml2::xml_name(second_child) == t2) next + if (!is.null(t3)) { + third_children <- xml2::xml_children(second_child) + for (third_child in third_children) { + if (!xml2::xml_name(third_child) == t3) next + if (!is.null(val3) && is.na(xml2::xml_attr(third_child, "val"))) next + if (!is.null(val3) && xml2::xml_attr(third_child, "val") != val3) next + if (!is.null(t4)) { + fourth_children <- xml2::xml_children(third_child) + for (fourth_child in fourth_children) { + if (!xml2::xml_name(fourth_child) == t4) next + if (!is.null(val4) && is.na(xml2::xml_attr(fourth_child, "val"))) next + if (xml2::xml_attr(fourth_child, "val") != val4) next + match_found <- TRUE + } + } else match_found <- TRUE + } + } else match_found <- TRUE + } + } else match_found <- TRUE + + if (match_found) { + style_data <- list( + num_chars - nchar(xml2::xml_text(first_child)) + 1, + num_chars + ) + xml_attr_names <- xml2::xml_attrs(first_child) %>% names() + if (length(xml_attr_names) & "id" %in% xml_attr_names) { + style_data <- c(style_data, list(xml2::xml_attrs(first_child)[["id"]])) + } + if (length(xml_attr_names) & "anchor" %in% xml_attr_names) { + style_data <- c(style_data, list(xml2::xml_attrs(first_child)[["anchor"]])) + } + if (as.character(i - num_blank) %in% names(smap)) { + prev_index <- length(smap[[as.character(i - num_blank)]]) + new_index <- prev_index + 1 + if (style_data[[1]] == + smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { + smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- style_data[[2]] + } else { + smap[[as.character(i - num_blank)]][[new_index]] <- style_data + } + } else { + smap[[as.character(i - num_blank)]] <- list( + `1` = style_data + ) + } + } + } + } + + smap %>% purrr::map(unique) +} + + +#' Generate markdown files from Word doc +#' +#' A Word document can be used to generate multiple markdown files. The main use +#' case is for use in reviewing often changing text when writing final output of +#' initiatives. It can be used standalone for adhoc purposes. +#' +#' @param md_flag How markdown file is flagged in Word doc; lines starting with +#' this will be taken as flags to assign following content to a markdown file +#' with name as everything after this +#' @param rv_dir Review directory +#' @param docx_file Output Word document file name +#' @param md_out_dir Output folder for markdown files +#' +#' @return Nothing, used for side effects only +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' word_to_md() +#' +#' # May be times when you want to use outside of the usual use case, e.g. +#' # generate markdown for adhoc purposes +#' word_to_md( +#' "my/adhoc/markdown", +#' "C:/Users/CYPHER/Downloads", +#' "adhoc.docx", +#' "C:/Users/CYPHER/Downloads" +#' )} +word_to_md <- function(md_flag = "markdown/", + rv_dir = "inst/review", + docx_file = "review.docx", + md_out_dir = "inst/review/temp") { + doc <- officer::read_docx(file.path(rv_dir, docx_file)) + doc_df <- officer::docx_summary(doc) + maps <- list( + bold_map = style_map(doc, "r", "rPr", "b"), + ital_map = style_map(doc, "r", "rPr", "i"), + hypl_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "Hyperlink"), + code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), + chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") + ) + + # This will find the rows to use for each md file + breaks <- doc_df %>% + dplyr::filter(startsWith(.data$text, md_flag)) %>% + dplyr::mutate( + md_file = .data$text, + begin = .data$doc_index + 1, + end = dplyr::lead(.data$doc_index) - 1, + .keep = "none" + ) %>% + tidyr::replace_na(list(end = nrow(doc_df))) + + # Iterate over the markdown filenames. The content for each file is transformed + # to apply styling and hyperlinks and then written to md_out_dir + purrr::pwalk( + breaks, + \(md_file, begin, end) { + # Each file has content from row number start to end + doc_df <- doc_df %>% + dplyr::filter(dplyr::between(.data$doc_index, begin, end)) + + # Apply any bold styling + for (row in dplyr::intersect(names(maps$bold_map), begin:end)) { + offset <- 0 + increment <- 4 + for (style_data in maps$bold_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + bold_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% + dplyr::mutate( + text = paste0( + substr(.data$text, 0, start - 1), + "__", + substr(.data$text, start, stop), + "__", + substr(.data$text, stop + 1, nchar(.data$text)) + ) + ) %>% + dplyr::pull(.data$text) + + doc_df <- doc_df %>% + dplyr::mutate( + text = replace( + .data$text, + .data$doc_index == rownum, + bold_applied + ) + ) + + # Add offsets to remaining maps + for (m in 2:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[i]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Apply any italics styling + for (row in dplyr::intersect(names(maps$ital_map), begin:end)) { + offset <- 0 + increment <- 2 + for (style_data in maps$ital_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + ital_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% + dplyr::mutate( + text = paste0( + substr(.data$text, 0, start - 1), + "_", + substr(.data$text, start, stop), + "_", + substr(.data$text, stop + 1, nchar(.data$text)) + ) + ) %>% + dplyr::pull(.data$text) + + doc_df <- doc_df %>% + dplyr::mutate( + text = replace( + .data$text, + .data$doc_index == rownum, + ital_applied + ) + ) + + # Add offsets to remaining maps + for (m in 3:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Add any hyperlinks + for (row in dplyr::intersect(names(maps$hypl_map), begin:end)) { + offset <- 0 + increment <- 4 + for (style_data in maps$hypl_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + invisible(utils::capture.output( + url <- doc$ + doc_obj$ + relationship()$ + show() %>% + dplyr::as_tibble() %>% + dplyr::filter(.data$id == style_data[[3]]) %>% + dplyr::pull(.data$target) + )) + url <- if (length(style_data) == 4) { + paste0(url, "#", style_data[[4]]) + } else { + url + } + + hypl_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% + dplyr::mutate( + text = paste0( + substr(.data$text, 0, start - 1), + "[", + substr(.data$text, start, stop), + "](", + url, + ")", + substr(.data$text, stop + 1, nchar(.data$text)) + ) + ) %>% + dplyr::pull(.data$text) + + doc_df <- doc_df %>% + dplyr::mutate( + text = replace( + .data$text, + .data$doc_index == rownum, + hypl_applied + ) + ) + + # Add offsets to remaining maps + for (m in 4:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + + increment + nchar(url) + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + + increment + nchar(url) + } + } + } + } + + offset <- offset + increment + nchar(url) + } + } + + # Apply any code (monospace font) styling + for (row in dplyr::intersect(names(maps$code_map), begin:end)) { + offset <- 0 + increment <- 8 + for (style_data in maps$code_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + + code_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% + dplyr::mutate( + text = paste0( + substr(.data$text, 0, start - 1), + "````", + substr(.data$text, start, stop), + "````", + substr(.data$text, stop + 1, nchar(.data$text)) + ) + ) %>% + dplyr::pull(.data$text) + + doc_df <- doc_df %>% + dplyr::mutate( + text = replace( + .data$text, + .data$doc_index == rownum, + code_applied + ) + ) + + # Add offsets to remaining maps + for (m in 5:5) { + if (row %in% names(maps[[m]])) { + for (i in seq(length(maps[[m]][[row]]))) { + if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { + maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + } + } + } + } + + offset <- offset + increment + } + } + + # Add any code (monospace font) hyperlinks + for (row in dplyr::intersect(names(maps$chyp_map), begin:end)) { + offset <- 0 + increment <- 12 + for (style_data in maps$chyp_map[[row]]) { + rownum <- as.integer(row) + start <- style_data[[1]] + offset + stop <- style_data[[2]] + offset + invisible(utils::capture.output( + url <- doc$ + doc_obj$ + relationship()$ + show() %>% + dplyr::as_tibble() %>% + dplyr::filter(.data$id == style_data[[3]]) %>% + dplyr::pull(.data$target) + )) + + chyp_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% + dplyr::mutate( + text = paste0( + substr(.data$text, 0, start - 1), + "[````", + substr(.data$text, start, stop), + "````](", + url, + ")", + substr(.data$text, stop + 1, nchar(.data$text)) + ) + ) %>% + dplyr::pull(.data$text) + + doc_df <- doc_df %>% + dplyr::mutate( + text = replace( + .data$text, + .data$doc_index == rownum, + chyp_applied + ) + ) + + offset <- offset + increment + nchar(url) + } + } + + # Numbered lists need special treatment, the XML is very convoluted... + numbering_xml <- file.path( + doc$package_dir, + "word", + "numbering.xml" + ) %>% xml2::read_xml() + + num_ids_alt_1 <- numbering_xml %>% + xml2::xml_find_all("//w:num[w:abstractNumId/@w:val='99411']") %>% + xml2::xml_attr("numId") %>% + as.numeric() + num_ids_alt_2 <- numbering_xml %>% + xml2::xml_find_all("//w:num[w:abstractNumId/@w:val='2']") %>% + xml2::xml_attr("numId") %>% + as.numeric() + num_ids <- c(num_ids_alt_1, num_ids_alt_2) + + # Add heading, bullet and list markup and find where to add blank lines + doc_df <- doc_df %>% + dplyr::mutate( + next_style = dplyr::lead(.data$style_name), + blank_after = ( + (.data$style_name != "Compact") | + (.data$style_name == "Compact" & + dplyr::lead(.data$style_name) != "Compact") + ) & + (!is.na(dplyr::lead(.data$style_name))), + style_name = dplyr::case_when( + .data$num_id %in% num_ids ~ "Numbering", + TRUE ~ .data$style_name + ), + text = dplyr::case_match( + .data$style_name, + "Heading 1" ~ paste0("# ", .data$text), + "heading 1" ~ paste0("# ", .data$text), + "Heading 2" ~ paste0("## ", .data$text), + "heading 2" ~ paste0("## ", .data$text), + "Heading 3" ~ paste0("### ", .data$text), + "heading 3" ~ paste0("### ", .data$text), + "Heading 4" ~ paste0("#### ", .data$text), + "heading 4" ~ paste0("#### ", .data$text), + "Compact" ~ paste0("- ", .data$text), + .default = .data$text + ) + ) %>% + dplyr::group_by(.data$num_id) %>% + dplyr::mutate( + text = dplyr::case_match( + .data$style_name, + "Numbering" ~ paste0(seq(dplyr::n()), ". ", .data$text), + .default = .data$text + ) + ) %>% + dplyr::ungroup() + + # Get the element indices which need a blank line afterward + needs_blank_after <- which(doc_df$blank_after) + + seq_len(length(which(doc_df$blank_after))) - 1 + + # Iterate over the indices and add a new blank row after each + purrr::walk( + needs_blank_after, + \(x) doc_df <<- dplyr::add_row(doc_df, text = "", .after = x) + ) + + # Write the markdown file + dir.create(md_out_dir, showWarnings = FALSE) + writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) + } + ) +} + +# Instructions ------------------------------------------------------------ +# 1. Get initial snapshots +# 2. Comment out the first code block +# 3. Uncomment the rest of the code +# 4. Whenever you make changes to these files, 'Run Tests' again +# 5. Any differences will be caught; run the below code in the console to review +# them in a shiny app (working dir is project root): +# testthat::snapshot_review('snapshot_tests/', "review/tests") +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# If there are some changes that were not intended, you should fix these in +# the temporary markdown files (review/temp). You can leave the diff viewer +# open and revert the changes directly by a simple copy and paste of the +# original content. Click 'Skip' once the reversions are complete. +# 7. Close the diff viewer once only skipped files remain. +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. +# 9. If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, uncomment it now so it is ready for the next set of +# changes. + +# gen_snaps <- function() { +# withr::with_dir(rprojroot::find_package_root_file(), { +# source("review/scripts/md_to_word.R") +# +# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") +# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") +# }) +# } + +# Generate snapshots ------------------------------------------------------ +# 1. To get the initial snapshot, run the below block, with an expectation for +# each reference file. Use 'Run Tests' button above right. +# You are basically saying "all these files should look as they are now in +# future". +# test_that("generate snapshots", { +# with_dir(find_package_root_file(), { +# source("review/scripts/md_to_word.R") +# +# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") +# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") +# }) +# }) + +# 2. Comment the above block out now + +# 3. Uncomment the below block, ready for future changes + + +# Compare markdown -------------------------------------------------------- +# 4. When you make changes to the files, use 'Run Tests' button above right +# test_that("compare markdown", { +# with_dir(find_package_root_file(), { +# source("review/scripts/word_to_md.R") +# +# expect_snapshot_file("review/temp/01_mod_the_first.md") +# expect_snapshot_file("review/temp/02_mod_the_second.md") +# }) +# }) + + +# Run diff viewer --------------------------------------------------------- +# 5. If any differences are present (there should be if you changed the files...) +# a diff viewer can be opened by running the code below in the console (you can +# open it in browser using the 'Show in new window' button in the Viewer pane) +# testthat::snapshot_review('snapshot_tests/', "review/tests") + + +# Review - accept or fix -------------------------------------------------- +# 6. Review the changes in each file one by one. +# If ALL changes in a file are as intended, then choose the 'Accept' option. +# +# If there are some changes that were not intended, you should fix these in +# the source files. You can leave the diff viewer open and revert the changes +# directly by a simple copy and paste of the original content. +# +# Click 'Skip' once the reversions are complete. + +# 7. Close the diff viewer once only skipped files remain. + +# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line +# source("review/scripts/word_to_md.R") +# then run this test file again with 'Run Tests'. You should expect no +# differences to be found now, but if there are then repeat step 6 to 8 until +# none are found. + + +# Replace markdown -------------------------------------------------------- +# 9. It is now safe to copy the revised markdown files from the review/temp folder +# over the app markdown files in inst/app/www/assets/markdown. +# +# If you had to comment out the line +# source("review/scripts/word_to_md.R") +# due to reversions, MAKE SURE to uncomment it now so it is ready for the next +# set of changes. \ No newline at end of file diff --git a/inst/review/scripts/md_to_word.R b/inst/review/scripts/md_to_word.R deleted file mode 100644 index 22d61e8..0000000 --- a/inst/review/scripts/md_to_word.R +++ /dev/null @@ -1,54 +0,0 @@ -library(rmarkdown) -library(officedown) -library(purrr) - -# Setup ------------------------------------------------------------------- - -md_dir <- "inst/app/www/assets/markdown" # Markdown dir -rv_dir <- "review" # Review directory -rmd_file <- "all_md.rmd" # Output rmarkdown -docx_file <- "review.docx" # Output Word doc -styles_dir <- "review/styles" # Styles dir -styles_rmd <- "draft-styles.rmd" # Creates Word template -styles_doc <- "draft-styles.docx" # Word template - -render( - file.path(styles_dir, styles_rmd), - output_dir = styles_dir, - output_file = styles_doc, - quiet = TRUE -) - - -# Create rmarkdown -------------------------------------------------------- - -# Get relative path of md files -md_files <- Sys.glob(file.path(md_dir, "*.md")) - -# Combine them into one rmarkdown file -all_md <- map(md_files, readLines) # List of md content -all_md <- map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add marker to md file -all_md <- reduce(all_md, c) # All content in one vector - -writeLines(all_md, file.path(rv_dir, rmd_file)) - - -# Create Word document ---------------------------------------------------- - -render( - file.path(rv_dir, rmd_file), - rdocx_document( - reference_docx = file.path(styles_dir, styles_doc), - toc = FALSE, - number_sections = FALSE - ), - output_dir = rv_dir, - output_file = docx_file, - quiet = TRUE -) - - -# Tidy up ----------------------------------------------------------------- - -unlink(file.path(styles_dir, styles_doc)) # Delete style doc -unlink(file.path(rv_dir, rmd_file)) # Delete combined rmd diff --git a/inst/review/scripts/word_to_md.R b/inst/review/scripts/word_to_md.R deleted file mode 100644 index 051d6eb..0000000 --- a/inst/review/scripts/word_to_md.R +++ /dev/null @@ -1,446 +0,0 @@ -library(officer) -library(purrr) -library(dplyr) -library(tidyr) -library(xml2) - -# Setup ------------------------------------------------------------------- - -md_dir <- "inst/app/www/assets/markdown" # Markdown dir -rv_dir <- "review" # Review directory -docx_file <- "review.docx" # Input Word doc -md_out_dir <- "review/temp" # Output markdown dir - - -# Get Word doc contents --------------------------------------------------- - -doc <- read_docx(file.path(rv_dir, docx_file)) -doc_df <- docx_summary(doc) - - -# Get style data ---------------------------------------------------------- - -# We need to create maps for each type of styling or hyperlink. -# These will be applied to the markdown generated from the Word doc -style_map <- function(doc, t1, t2 = NULL, t3 = NULL, val3 = NULL, t4 = NULL, val4 = NULL) { - smap <- list() - num_blank <- 0 - num_elements <- length(doc) - - for (i in 1:num_elements) { - doc$officer_cursor$which <- i - num_chars <- 0 - parent <- docx_current_block_xml(doc) - first_children <- xml_children(parent) - if(xml_text(parent) == "") num_blank <- num_blank + 1 - for (first_child in first_children) { - match_found <- FALSE - num_chars <- num_chars + nchar(xml_text(first_child)) - if (!xml_name(first_child) == t1) next - if (!is.null(t2)) { - second_children <- xml_children(first_child) - for (second_child in second_children) { - if (!xml_name(second_child) == t2) next - if (!is.null(t3)) { - third_children <- xml_children(second_child) - for (third_child in third_children) { - if (!xml_name(third_child) == t3) next - if (!is.null(val3) && is.na(xml_attr(third_child, "val"))) next - if (!is.null(val3) && xml_attr(third_child, "val") != val3) next - if (!is.null(t4)) { - fourth_children <- xml_children(third_child) - for (fourth_child in fourth_children) { - if (!xml_name(fourth_child) == t4) next - if (!is.null(val4) && is.na(xml_attr(fourth_child, "val"))) next - if (xml_attr(fourth_child, "val") != val4) next - match_found <- TRUE - } - } else match_found <- TRUE - } - } else match_found <- TRUE - } - } else match_found <- TRUE - - if (match_found) { - style_data <- list( - num_chars - nchar(xml_text(first_child)) + 1, - num_chars - ) - xml_attr_names <- xml_attrs(first_child) %>% names() - if (length(xml_attr_names) & "id" %in% xml_attr_names) { - style_data <- c(style_data, list(xml_attrs(first_child)[["id"]])) - } - if (length(xml_attr_names) & "anchor" %in% xml_attr_names) { - style_data <- c(style_data, list(xml_attrs(first_child)[["anchor"]])) - } - if (as.character(i - num_blank) %in% names(smap)) { - prev_index <- length(smap[[as.character(i - num_blank)]]) - new_index <- prev_index + 1 - if (style_data[[1]] == - smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { - smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- style_data[[2]] - } else { - smap[[as.character(i - num_blank)]][[new_index]] <- style_data - } - } else { - smap[[as.character(i - num_blank)]] <- list( - `1` = style_data - ) - } - } - } - } - - smap %>% map(unique) -} - -maps <- list( - bold_map = style_map(doc, "r", "rPr", "b"), - ital_map = style_map(doc, "r", "rPr", "i"), - hypl_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "Hyperlink"), - code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), - chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") -) - - -# Compute file breakpoints ------------------------------------------------ - -# This will find the rows to use for each md file -breaks <- doc_df %>% - filter(startsWith(text, md_dir)) %>% - mutate( - md_file = text, - begin = doc_index + 1, - end = lead(doc_index) - 1, - .keep = "none" - ) %>% - replace_na(list(end = nrow(doc_df))) - - -# Create markdown files --------------------------------------------------- - -# Iterate over the markdown filenames. The content for each file is transformed -# to apply styling and hyperlinks and then written to md_out_dir -pwalk( - breaks, - \(md_file, begin, end) { - # Each file has content from row number start to end - doc_df <- doc_df %>% - filter(between(doc_index, begin, end)) - - # Apply any bold styling - for (row in intersect(names(maps$bold_map), begin:end)) { - offset <- 0 - increment <- 4 - for (style_data in maps$bold_map[[row]]) { - rownum <- as.integer(row) - start <- style_data[[1]] + offset - stop <- style_data[[2]] + offset - - bold_applied <- doc_df %>% - filter(doc_index == rownum) %>% - mutate( - text = paste0( - substr(text, 0, start - 1), - "__", - substr(text, start, stop), - "__", - substr(text, stop + 1, nchar(text)) - ) - ) %>% - pull(text) - - doc_df <- doc_df %>% - mutate( - text = replace( - text, - doc_index == rownum, - bold_applied - ) - ) - - # Add offsets to remaining maps - for (m in 2:5) { - if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { - if ((stop + offset) <= maps[[i]][[row]][[i]][[1]]) { - maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment - maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment - } - } - } - } - - offset <- offset + increment - } - } - - # Apply any italics styling - for (row in intersect(names(maps$ital_map), begin:end)) { - offset <- 0 - increment <- 2 - for (style_data in maps$ital_map[[row]]) { - rownum <- as.integer(row) - start <- style_data[[1]] + offset - stop <- style_data[[2]] + offset - - ital_applied <- doc_df %>% - filter(doc_index == rownum) %>% - mutate( - text = paste0( - substr(text, 0, start - 1), - "_", - substr(text, start, stop), - "_", - substr(text, stop + 1, nchar(text)) - ) - ) %>% - pull(text) - - doc_df <- doc_df %>% - mutate( - text = replace( - text, - doc_index == rownum, - ital_applied - ) - ) - - # Add offsets to remaining maps - for (m in 3:5) { - if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { - if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { - maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment - maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment - } - } - } - } - - offset <- offset + increment - } - } - - # Add any hyperlinks - for (row in intersect(names(maps$hypl_map), begin:end)) { - offset <- 0 - increment <- 4 - for (style_data in maps$hypl_map[[row]]) { - rownum <- as.integer(row) - start <- style_data[[1]] + offset - stop <- style_data[[2]] + offset - invisible(capture.output( - url <- doc$ - doc_obj$ - relationship()$ - show() %>% - as_tibble() %>% - filter(id == style_data[[3]]) %>% - pull(target) - )) - url <- if (length(style_data) == 4) { - paste0(url, "#", style_data[[4]]) - } else { - url - } - - hypl_applied <- doc_df %>% - filter(doc_index == rownum) %>% - mutate( - text = paste0( - substr(text, 0, start - 1), - "[", - substr(text, start, stop), - "](", - url, - ")", - substr(text, stop + 1, nchar(text)) - ) - ) %>% - pull(text) - - doc_df <- doc_df %>% - mutate( - text = replace( - text, - doc_index == rownum, - hypl_applied - ) - ) - - # Add offsets to remaining maps - for (m in 4:5) { - if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { - if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { - maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + nchar(url) - maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment + nchar(url) - } - } - } - } - - offset <- offset + increment + nchar(url) - } - } - - # Apply any code (monospace font) styling - for (row in intersect(names(maps$code_map), begin:end)) { - offset <- 0 - increment <- 8 - for (style_data in maps$code_map[[row]]) { - rownum <- as.integer(row) - start <- style_data[[1]] + offset - stop <- style_data[[2]] + offset - - code_applied <- doc_df %>% - filter(doc_index == rownum) %>% - mutate( - text = paste0( - substr(text, 0, start - 1), - "````", - substr(text, start, stop), - "````", - substr(text, stop + 1, nchar(text)) - ) - ) %>% - pull(text) - - doc_df <- doc_df %>% - mutate( - text = replace( - text, - doc_index == rownum, - code_applied - ) - ) - - # Add offsets to remaining maps - for (m in 5:5) { - if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { - if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { - maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment - maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment - } - } - } - } - - offset <- offset + increment - } - } - - # Add any code (monospace font) hyperlinks - for (row in intersect(names(maps$chyp_map), begin:end)) { - offset <- 0 - increment <- 12 - for (style_data in maps$chyp_map[[row]]) { - rownum <- as.integer(row) - start <- style_data[[1]] + offset - stop <- style_data[[2]] + offset - invisible(capture.output( - url <- doc$ - doc_obj$ - relationship()$ - show() %>% - as_tibble() %>% - filter(id == style_data[[3]]) %>% - pull(target) - )) - - chyp_applied <- doc_df %>% - filter(doc_index == rownum) %>% - mutate( - text = paste0( - substr(text, 0, start - 1), - "[````", - substr(text, start, stop), - "````](", - url, - ")", - substr(text, stop + 1, nchar(text)) - ) - ) %>% - pull(text) - - doc_df <- doc_df %>% - mutate( - text = replace( - text, - doc_index == rownum, - chyp_applied - ) - ) - - offset <- offset + increment + nchar(url) - } - } - - # Numbered lists need special treatment, the XML is very convoluted... - numbering_xml <- file.path( - doc$package_dir, - "word", - "numbering.xml" - ) %>% read_xml() - - num_ids_alt_1 <- numbering_xml %>% - xml_find_all("//w:num[w:abstractNumId/@w:val='99411']") %>% - xml_attr("numId") %>% - as.numeric() - num_ids_alt_2 <- numbering_xml %>% - xml_find_all("//w:num[w:abstractNumId/@w:val='2']") %>% - xml_attr("numId") %>% - as.numeric() - num_ids <- c(num_ids_alt_1, num_ids_alt_2) - - # Add heading, bullet and list markup and find where to add blank lines - doc_df <- doc_df %>% - mutate( - next_style = lead(style_name), - blank_after = ( - (style_name != "Compact") | - (style_name == "Compact" & lead(style_name) != "Compact") - ) & - (!is.na(lead(style_name))), - style_name = case_when( - num_id %in% num_ids ~ "Numbering", - TRUE ~ style_name - ), - text = case_match( - style_name, - "Heading 1" ~ paste0("# ", text), - "heading 1" ~ paste0("# ", text), - "Heading 2" ~ paste0("## ", text), - "heading 2" ~ paste0("## ", text), - "Heading 3" ~ paste0("### ", text), - "heading 3" ~ paste0("### ", text), - "Heading 4" ~ paste0("#### ", text), - "heading 4" ~ paste0("#### ", text), - "Compact" ~ paste0("- ", text), - .default = text - ) - ) %>% - group_by(num_id) %>% - mutate( - text = case_match( - style_name, - "Numbering" ~ paste0(seq(n()), ". ", text), - .default = text - ) - ) %>% - ungroup() - - # Get the element indices which need a blank line afterward - needs_blank_after <- which(doc_df$blank_after) + - seq_len(length(which(doc_df$blank_after))) - 1 - - # Iterate over the indices and add a new blank row after each - walk(needs_blank_after, \(x) doc_df <<- add_row(doc_df, text = "", .after = x)) - - # Write the markdown file - dir.create(md_out_dir, showWarnings = FALSE) - writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) - } -) diff --git a/man/md_to_word.Rd b/man/md_to_word.Rd index bdbaa30..5f96f50 100644 --- a/man/md_to_word.Rd +++ b/man/md_to_word.Rd @@ -1,12 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils-review.R +% Please edit documentation in R/utils_review.R \name{md_to_word} \alias{md_to_word} \title{Generate Word doc from markdown files} \usage{ md_to_word( md_dir = "inst/app/www/assets/markdown", - rv_dir = "review", + rv_dir = "inst/review", docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd" ) @@ -29,6 +29,7 @@ main use case is for use in reviewing often changing text when writing final output of initiatives. It can be used standalone for adhoc purposes. } \examples{ +\dontrun{ # For standard use case, default args are fine md_to_word() @@ -39,5 +40,5 @@ md_to_word( "C:/Users/CYPHER/Downloads", "adhoc.docx", system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") -) +)} } diff --git a/man/word_to_md.Rd b/man/word_to_md.Rd new file mode 100644 index 0000000..04ae073 --- /dev/null +++ b/man/word_to_md.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{word_to_md} +\alias{word_to_md} +\title{Generate markdown files from Word doc} +\usage{ +word_to_md( + md_flag = "markdown/", + rv_dir = "inst/review", + docx_file = "review.docx", + md_out_dir = "inst/review/temp" +) +} +\arguments{ +\item{md_flag}{How markdown file is flagged in Word doc; lines starting with +this will be taken as flags to assign following content to a markdown file +with name as everything after this} + +\item{rv_dir}{Review directory} + +\item{docx_file}{Output Word document file name} + +\item{md_out_dir}{Output folder for markdown files} +} +\value{ +Nothing, used for side effects only +} +\description{ +A Word document can be used to generate multiple markdown files. The main use +case is for use in reviewing often changing text when writing final output of +initiatives. It can be used standalone for adhoc purposes. +} +\examples{ +\dontrun{ +# For standard use case, default args are fine +word_to_md() + +# May be times when you want to use outside of the usual use case, e.g. +# generate markdown for adhoc purposes +word_to_md( + "my/adhoc/markdown", + "C:/Users/CYPHER/Downloads", + "adhoc.docx", + "C:/Users/CYPHER/Downloads" +)} +} diff --git a/tests/testthat/_snaps/utils_review.md b/tests/testthat/_snaps/utils_review.md new file mode 100644 index 0000000..e33d463 --- /dev/null +++ b/tests/testthat/_snaps/utils_review.md @@ -0,0 +1,128 @@ +# style_map generates expected maps + + Code + style_maps + Output + $bold_map + $bold_map$`6` + $bold_map$`6`[[1]] + $bold_map$`6`[[1]][[1]] + [1] 1 + + $bold_map$`6`[[1]][[2]] + [1] 9 + + + + $bold_map$`24` + $bold_map$`24`[[1]] + $bold_map$`24`[[1]][[1]] + [1] 1 + + $bold_map$`24`[[1]][[2]] + [1] 9 + + + + + $ital_map + $ital_map$`7` + $ital_map$`7`[[1]] + $ital_map$`7`[[1]][[1]] + [1] 1 + + $ital_map$`7`[[1]][[2]] + [1] 15 + + + + $ital_map$`25` + $ital_map$`25`[[1]] + $ital_map$`25`[[1]][[1]] + [1] 1 + + $ital_map$`25`[[1]][[2]] + [1] 15 + + + + + $hypl_map + $hypl_map$`17` + $hypl_map$`17`[[1]] + $hypl_map$`17`[[1]][[1]] + [1] 1 + + $hypl_map$`17`[[1]][[2]] + [1] 14 + + $hypl_map$`17`[[1]][[3]] + [1] "rId20" + + + + $hypl_map$`35` + $hypl_map$`35`[[1]] + $hypl_map$`35`[[1]][[1]] + [1] 1 + + $hypl_map$`35`[[1]][[2]] + [1] 14 + + $hypl_map$`35`[[1]][[3]] + [1] "rId20" + + + + + $code_map + $code_map$`16` + $code_map$`16`[[1]] + $code_map$`16`[[1]][[1]] + [1] 21 + + $code_map$`16`[[1]][[2]] + [1] 24 + + + + $code_map$`34` + $code_map$`34`[[1]] + $code_map$`34`[[1]][[1]] + [1] 21 + + $code_map$`34`[[1]][[2]] + [1] 24 + + + + + $chyp_map + $chyp_map$`18` + $chyp_map$`18`[[1]] + $chyp_map$`18`[[1]][[1]] + [1] 1 + + $chyp_map$`18`[[1]][[2]] + [1] 14 + + $chyp_map$`18`[[1]][[3]] + [1] "rId21" + + + + $chyp_map$`36` + $chyp_map$`36`[[1]] + $chyp_map$`36`[[1]][[1]] + [1] 1 + + $chyp_map$`36`[[1]][[2]] + [1] 14 + + $chyp_map$`36`[[1]][[3]] + [1] "rId21" + + + + + diff --git a/tests/testthat/_snaps/utils_review/01_md_file.md b/tests/testthat/_snaps/utils_review/01_md_file.md new file mode 100644 index 0000000..0f89778 --- /dev/null +++ b/tests/testthat/_snaps/utils_review/01_md_file.md @@ -0,0 +1,29 @@ +# H1 + +## H2 + +### H3 + +#### H4 + +__bold text__ + +_italicized text_ + +Numbered list + +1. First item +2. Second item +3. Third item + +Bulleted list + +- First item +- Second item +- Third item + +Here is some inline ````code````. + +[Markdown Guide](https://www.markdownguide.org) + +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) diff --git a/tests/testthat/_snaps/utils_review/02_md_file.md b/tests/testthat/_snaps/utils_review/02_md_file.md new file mode 100644 index 0000000..0f89778 --- /dev/null +++ b/tests/testthat/_snaps/utils_review/02_md_file.md @@ -0,0 +1,29 @@ +# H1 + +## H2 + +### H3 + +#### H4 + +__bold text__ + +_italicized text_ + +Numbered list + +1. First item +2. Second item +3. Third item + +Bulleted list + +- First item +- Second item +- Third item + +Here is some inline ````code````. + +[Markdown Guide](https://www.markdownguide.org) + +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) diff --git a/inst/review/review.docx b/tests/testthat/review.docx similarity index 53% rename from inst/review/review.docx rename to tests/testthat/review.docx index 97449d41d7cdeb69367aa96447b5b62338df35de..b7c1205b5779d2cfeddab21e26be6527614a5c21 100644 GIT binary patch delta 3032 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 diff --git a/tests/testthat/test-utils-review.R b/tests/testthat/test-utils-review.R deleted file mode 100644 index 6219667..0000000 --- a/tests/testthat/test-utils-review.R +++ /dev/null @@ -1,87 +0,0 @@ -test_that("md_to_word generates expected Word doc", { - throwaway <- withr::local_tempdir(pattern = "throwaway") - - md_dir <- file.path(throwaway, "markdown") - dir.create(md_dir) - - md_lines <- c( - "# H1 ", - "## H2 ", - "### H3 ", - "#### H4 ", - "__bold text__ ", - "_italicized text_ ", - "1. First item ", - "2. Second item ", - "3. Third item ", - "- First item ", - "- Second item ", - "- Third item ", - "Here is some inline ````code````. ", - "[Markdown Guide](https://www.markdownguide.org) ", - "[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) " - ) - - md_file1 <- writeLines( - md_lines, - file.path(md_dir, "01_md_file.md") - ) - - md_file2 <- writeLines( - md_lines, - file.path(md_dir, "02_md_file.md") - ) - - rv_dir <- file.path(throwaway, "review") - - docx_file <- "review.docx" - - styles_rmd <- system.file( - "review", "styles", "draft-styles.rmd", - package = "nhsbsaShinyR" - ) - - docx_path <- md_to_word( - md_dir = md_dir, - rv_dir = rv_dir, - docx_file = docx_file, - styles_rmd = styles_rmd - ) - - docx_path <- file.path(rv_dir, docx_file) - - # Need to use same created/modified date each run, or output doc will not - # match snapshot - docx <- officer::read_docx(docx_path) - - props_file <- file.path( - docx$package_dir, - "docProps", - "core.xml" - ) - props_xml <- xml2::read_xml(props_file) - props_xml <- xml2::xml_find_all( - props_xml, - "//*[starts-with(name(), 'dcterms')]" - ) - xml2::xml_text(props_xml) <- c("2023-11-07T16:07:06Z") - - xml2::write_xml( - xml2::xml_root(props_xml), - file.path( - docx$package_dir, - "docProps", - "core.xml" - ) - ) - - tryCatch( - expect_snapshot_file(docx_path), - error = \(e) { - cat("\n\n", waldo::compare( - officer::read_docx("_snaps/utils-review/review.docx"), - officer::read_docx("_snaps/utils-review/review.new.docx") - ), sep = "\n") - } - ) -}) diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R new file mode 100644 index 0000000..721e9ec --- /dev/null +++ b/tests/testthat/test-utils_review.R @@ -0,0 +1,78 @@ +test_that("md_to_word generates expected Word doc", { + docx_file <- "review.docx" + md_dir <- local_create_md() + rv_dir <- gsub("markdown", "review", md_dir) + docx_path <- local_create_word_doc( + md_dir = md_dir, + rv_dir = rv_dir, + docx_file = docx_file + ) + + # When run for first time, just save the generated Word doc for future use + # Note the test will essentially be comparing this doc to itself on that first + # run! + if (!file.exists(docx_file)) file.copy(docx_path, docx_file) + + # Get docx objects for comparisons + old <- officer::read_docx(docx_file) + new <- officer::read_docx(docx_path) + + comp_structure <- waldo::compare(new, old) + + # Due to how officer::read_docx works, we expect there to always be + # differences in the package_dir and doc_properties$data - if there are more + # than 2 diffs, something unexpected is also different. + # If unexpected number of diffs, print out the comparison for ease of seeing + # where the fail is. + if (length(comp_structure) > 2) cat("\n\n", comp_structure, sep = "\n") + + expect_lte(length(comp_structure), 2) + + # Since read_docx only has pointers to the actual text content, also need to + # compare the content separately. + expect_equal(officer::docx_summary(new), officer::docx_summary(old)) +}) + + +test_that("word_to_md generates expected markdown files", { + docx_file <- "review.docx" + md_dir <- local_create_md() + rv_dir <- gsub("markdown", "review", md_dir) + docx_path <- local_create_word_doc( + md_dir = md_dir, + rv_dir = rv_dir, + docx_file = docx_file + ) + + word_to_md( + rv_dir = rv_dir, + docx_file = docx_file, + md_out_dir = md_dir + ) + + expect_snapshot_file(file.path(md_dir, "01_md_file.md")) + expect_snapshot_file(file.path(md_dir, "02_md_file.md")) +}) + + +test_that("style_map generates expected maps", { + docx_file <- "review.docx" + md_dir <- local_create_md() + rv_dir <- gsub("markdown", "review", md_dir) + docx_path <- local_create_word_doc( + md_dir = md_dir, + rv_dir = rv_dir, + docx_file = docx_file + ) + doc <- officer::read_docx(docx_path) + + style_maps <- list( + bold_map = style_map(doc, "r", "rPr", "b"), + ital_map = style_map(doc, "r", "rPr", "i"), + hypl_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "Hyperlink"), + code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), + chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") + ) + + expect_snapshot(style_maps) +}) \ No newline at end of file From 81644246f83c2753a877da43c14c7b5b2c282b26 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 17 Nov 2023 14:57:32 +0000 Subject: [PATCH 19/32] Handle lints + attempt to get temp files from R CMD check GHA --- .github/workflows/R-CMD-check.yaml | 3 + R/test-helpers.R | 14 +- R/utils_review.R | 333 +++++++++++------------------ inst/review/tests/snapshot_tests.R | 42 ++-- man/md_to_word.Rd | 2 +- tests/testthat/test-utils_review.R | 24 +-- 6 files changed, 168 insertions(+), 250 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a4b4ac3..8b21304 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -46,3 +46,6 @@ jobs: with: name: ${{ runner.os }}-r${{ matrix.config.r }}-results path: check + + - name: Download all workflow run artifacts + uses: actions/download-artifact@v3 diff --git a/R/test-helpers.R b/R/test-helpers.R index e0c9923..0368f53 100644 --- a/R/test-helpers.R +++ b/R/test-helpers.R @@ -2,7 +2,7 @@ local_create_md <- function(temp_dir = tempdir(), env = parent.frame()) { md_dir <- file.path(temp_dir, "markdown") dir.create(md_dir, showWarnings = FALSE) withr::defer(unlink(temp_dir), envir = env) - + md_lines <- c( "# H1", "## H2", @@ -30,17 +30,17 @@ local_create_md <- function(temp_dir = tempdir(), env = parent.frame()) { "", "[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR)" ) - + writeLines( md_lines, file.path(md_dir, "01_md_file.md") ) - + writeLines( md_lines, file.path(md_dir, "02_md_file.md") ) - + md_dir } @@ -48,18 +48,18 @@ local_create_md <- function(temp_dir = tempdir(), env = parent.frame()) { local_create_word_doc <- function(temp_dir = tempdir(), md_dir, rv_dir, docx_file, env = parent.frame()) { withr::defer(unlink(temp_dir), envir = env) - + styles_rmd <- system.file( "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" ) - + docx_path <- md_to_word( md_dir = md_dir, rv_dir = rv_dir, docx_file = docx_file, styles_rmd = styles_rmd ) - + docx_path } diff --git a/R/utils_review.R b/R/utils_review.R index 8901818..4f88a53 100644 --- a/R/utils_review.R +++ b/R/utils_review.R @@ -1,6 +1,6 @@ #' Generate Word doc from markdown files -#' -#' All markdown files in a folder can be used to generate a Word document. The +#' +#' All markdown files in a folder can be used to generate a Word document. The #' main use case is for use in reviewing often changing text when writing final #' output of initiatives. It can be used standalone for adhoc purposes. #' @@ -25,9 +25,9 @@ #' "adhoc.docx", #' system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") #' )} -md_to_word <- function(md_dir = "inst/app/www/assets/markdown", +md_to_word <- function(md_dir = "inst/app/www/assets/markdown", rv_dir = "inst/review", - docx_file = "review.docx", + docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd") { styles_doc <- rmarkdown::render( styles_rmd, @@ -35,23 +35,23 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", output_file = tempfile(), quiet = TRUE ) - + # Get paths of md files md_files <- Sys.glob(file.path(md_dir, "*.md")) - + # Combine them into one rmarkdown file all_md <- purrr::map(md_files, readLines) # List of md content - + # Keep only parent folder and file name md_files <- file.path( rev(strsplit(dirname(md_files), "/")[[1]])[[1]], basename(md_files) ) - + all_md <- purrr::map2(all_md, md_files, \(x, y) c(y, "", x, "")) # Add file marker all_md <- purrr::reduce(all_md, c) # All content in one vector writeLines(all_md, file.path(tempdir(), "all_md.rmd")) - + # Create Word document rmarkdown::render( file.path(tempdir(), "all_md.rmd"), @@ -64,7 +64,7 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", output_file = docx_file, quiet = TRUE ) - + # Return path of generated Word doc file.path(rv_dir, docx_file) } @@ -102,19 +102,21 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", #' style_map(doc, "r", "rPr", "rStyle", "VerbatimChar") # monospace code #' style_map( #' doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar" -#' ) # monospace code hyperlinks -style_map <- function(doc, t1, t2 = NULL, +#' ) # monospace code hyperlinks +# Begin Exclude Linting +style_map <- function(doc, t1, t2 = NULL, t3 = NULL, val3 = NULL, t4 = NULL, val4 = NULL) { + # End Exclude Linting smap <- list() num_blank <- 0 - - for (i in 1:length(doc)) { + + for (i in seq_along(doc)) { doc$officer_cursor$which <- i num_chars <- 0 parent <- officer::docx_current_block_xml(doc) first_children <- xml2::xml_children(parent) - if(xml2::xml_text(parent) == "") num_blank <- num_blank + 1 + if (xml2::xml_text(parent) == "") num_blank <- num_blank + 1 for (first_child in first_children) { match_found <- FALSE num_chars <- num_chars + nchar(xml2::xml_text(first_child)) @@ -137,29 +139,35 @@ style_map <- function(doc, t1, t2 = NULL, if (xml2::xml_attr(fourth_child, "val") != val4) next match_found <- TRUE } - } else match_found <- TRUE + } else { + match_found <- TRUE + } } - } else match_found <- TRUE + } else { + match_found <- TRUE + } } - } else match_found <- TRUE - + } else { + match_found <- TRUE + } + if (match_found) { style_data <- list( num_chars - nchar(xml2::xml_text(first_child)) + 1, num_chars ) xml_attr_names <- xml2::xml_attrs(first_child) %>% names() - if (length(xml_attr_names) & "id" %in% xml_attr_names) { + if (length(xml_attr_names) && "id" %in% xml_attr_names) { style_data <- c(style_data, list(xml2::xml_attrs(first_child)[["id"]])) } - if (length(xml_attr_names) & "anchor" %in% xml_attr_names) { + if (length(xml_attr_names) && "anchor" %in% xml_attr_names) { style_data <- c(style_data, list(xml2::xml_attrs(first_child)[["anchor"]])) } if (as.character(i - num_blank) %in% names(smap)) { prev_index <- length(smap[[as.character(i - num_blank)]]) new_index <- prev_index + 1 - if (style_data[[1]] == - smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { + if (style_data[[1]] == + smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- style_data[[2]] } else { smap[[as.character(i - num_blank)]][[new_index]] <- style_data @@ -172,7 +180,7 @@ style_map <- function(doc, t1, t2 = NULL, } } } - + smap %>% purrr::map(unique) } @@ -206,10 +214,12 @@ style_map <- function(doc, t1, t2 = NULL, #' "adhoc.docx", #' "C:/Users/CYPHER/Downloads" #' )} -word_to_md <- function(md_flag = "markdown/", +# Begin Exclude Linting +word_to_md <- function(md_flag = "markdown/", rv_dir = "inst/review", docx_file = "review.docx", md_out_dir = "inst/review/temp") { + # End Exclude Linting doc <- officer::read_docx(file.path(rv_dir, docx_file)) doc_df <- officer::docx_summary(doc) maps <- list( @@ -219,7 +229,7 @@ word_to_md <- function(md_flag = "markdown/", code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") ) - + # This will find the rows to use for each md file breaks <- doc_df %>% dplyr::filter(startsWith(.data$text, md_flag)) %>% @@ -230,7 +240,7 @@ word_to_md <- function(md_flag = "markdown/", .keep = "none" ) %>% tidyr::replace_na(list(end = nrow(doc_df))) - + # Iterate over the markdown filenames. The content for each file is transformed # to apply styling and hyperlinks and then written to md_out_dir purrr::pwalk( @@ -239,7 +249,7 @@ word_to_md <- function(md_flag = "markdown/", # Each file has content from row number start to end doc_df <- doc_df %>% dplyr::filter(dplyr::between(.data$doc_index, begin, end)) - + # Apply any bold styling for (row in dplyr::intersect(names(maps$bold_map), begin:end)) { offset <- 0 @@ -248,9 +258,9 @@ word_to_md <- function(md_flag = "markdown/", rownum <- as.integer(row) start <- style_data[[1]] + offset stop <- style_data[[2]] + offset - - bold_applied <- doc_df %>% - dplyr::filter(.data$doc_index == rownum) %>% + + bold_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% dplyr::mutate( text = paste0( substr(.data$text, 0, start - 1), @@ -259,22 +269,22 @@ word_to_md <- function(md_flag = "markdown/", "__", substr(.data$text, stop + 1, nchar(.data$text)) ) - ) %>% + ) %>% dplyr::pull(.data$text) - - doc_df <- doc_df %>% + + doc_df <- doc_df %>% dplyr::mutate( text = replace( - .data$text, - .data$doc_index == rownum, + .data$text, + .data$doc_index == rownum, bold_applied ) ) - + # Add offsets to remaining maps for (m in 2:5) { if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { + for (i in seq_along(maps[[m]][[row]])) { if ((stop + offset) <= maps[[i]][[row]][[i]][[1]]) { maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment @@ -282,11 +292,11 @@ word_to_md <- function(md_flag = "markdown/", } } } - + offset <- offset + increment } } - + # Apply any italics styling for (row in dplyr::intersect(names(maps$ital_map), begin:end)) { offset <- 0 @@ -295,9 +305,9 @@ word_to_md <- function(md_flag = "markdown/", rownum <- as.integer(row) start <- style_data[[1]] + offset stop <- style_data[[2]] + offset - - ital_applied <- doc_df %>% - dplyr::filter(.data$doc_index == rownum) %>% + + ital_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% dplyr::mutate( text = paste0( substr(.data$text, 0, start - 1), @@ -306,22 +316,22 @@ word_to_md <- function(md_flag = "markdown/", "_", substr(.data$text, stop + 1, nchar(.data$text)) ) - ) %>% + ) %>% dplyr::pull(.data$text) - - doc_df <- doc_df %>% + + doc_df <- doc_df %>% dplyr::mutate( text = replace( - .data$text, - .data$doc_index == rownum, + .data$text, + .data$doc_index == rownum, ital_applied ) ) - + # Add offsets to remaining maps for (m in 3:5) { if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { + for (i in seq_along(maps[[m]][[row]])) { if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment @@ -329,11 +339,11 @@ word_to_md <- function(md_flag = "markdown/", } } } - + offset <- offset + increment } } - + # Add any hyperlinks for (row in dplyr::intersect(names(maps$hypl_map), begin:end)) { offset <- 0 @@ -342,21 +352,23 @@ word_to_md <- function(md_flag = "markdown/", rownum <- as.integer(row) start <- style_data[[1]] + offset stop <- style_data[[2]] + offset - invisible(utils::capture.output( - url <- doc$ - doc_obj$ - relationship()$ - show() %>% - dplyr::as_tibble() %>% - dplyr::filter(.data$id == style_data[[3]]) %>% - dplyr::pull(.data$target) - )) + invisible( + utils::capture.output( + url <- doc$ + doc_obj$ + relationship()$ # Exclude Linting + show() %>% + dplyr::as_tibble() %>% + dplyr::filter(.data$id == style_data[[3]]) %>% + dplyr::pull(.data$target) + ) + ) url <- if (length(style_data) == 4) { paste0(url, "#", style_data[[4]]) } else { url } - + hypl_applied <- doc_df %>% dplyr::filter(.data$doc_index == rownum) %>% dplyr::mutate( @@ -371,7 +383,7 @@ word_to_md <- function(md_flag = "markdown/", ) ) %>% dplyr::pull(.data$text) - + doc_df <- doc_df %>% dplyr::mutate( text = replace( @@ -380,11 +392,11 @@ word_to_md <- function(md_flag = "markdown/", hypl_applied ) ) - + # Add offsets to remaining maps for (m in 4:5) { if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { + for (i in seq_along(maps[[m]][[row]])) { if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment + nchar(url) @@ -394,11 +406,11 @@ word_to_md <- function(md_flag = "markdown/", } } } - + offset <- offset + increment + nchar(url) } } - + # Apply any code (monospace font) styling for (row in dplyr::intersect(names(maps$code_map), begin:end)) { offset <- 0 @@ -407,9 +419,9 @@ word_to_md <- function(md_flag = "markdown/", rownum <- as.integer(row) start <- style_data[[1]] + offset stop <- style_data[[2]] + offset - - code_applied <- doc_df %>% - dplyr::filter(.data$doc_index == rownum) %>% + + code_applied <- doc_df %>% + dplyr::filter(.data$doc_index == rownum) %>% dplyr::mutate( text = paste0( substr(.data$text, 0, start - 1), @@ -418,22 +430,22 @@ word_to_md <- function(md_flag = "markdown/", "````", substr(.data$text, stop + 1, nchar(.data$text)) ) - ) %>% + ) %>% dplyr::pull(.data$text) - - doc_df <- doc_df %>% + + doc_df <- doc_df %>% dplyr::mutate( text = replace( - .data$text, - .data$doc_index == rownum, + .data$text, + .data$doc_index == rownum, code_applied ) ) - + # Add offsets to remaining maps for (m in 5:5) { if (row %in% names(maps[[m]])) { - for (i in seq(length(maps[[m]][[row]]))) { + for (i in seq_along(maps[[m]][[row]])) { if ((stop + offset) <= maps[[m]][[row]][[i]][[1]]) { maps[[m]][[row]][[i]][[1]] <<- maps[[m]][[row]][[i]][[1]] + increment maps[[m]][[row]][[i]][[2]] <<- maps[[m]][[row]][[i]][[2]] + increment @@ -441,11 +453,11 @@ word_to_md <- function(md_flag = "markdown/", } } } - + offset <- offset + increment } } - + # Add any code (monospace font) hyperlinks for (row in dplyr::intersect(names(maps$chyp_map), begin:end)) { offset <- 0 @@ -454,16 +466,18 @@ word_to_md <- function(md_flag = "markdown/", rownum <- as.integer(row) start <- style_data[[1]] + offset stop <- style_data[[2]] + offset - invisible(utils::capture.output( - url <- doc$ - doc_obj$ - relationship()$ - show() %>% - dplyr::as_tibble() %>% - dplyr::filter(.data$id == style_data[[3]]) %>% - dplyr::pull(.data$target) - )) - + invisible( + utils::capture.output( + url <- doc$ + doc_obj$ + relationship()$ # Exclude Linting + show() %>% + dplyr::as_tibble() %>% + dplyr::filter(.data$id == style_data[[3]]) %>% + dplyr::pull(.data$target) + ) + ) + chyp_applied <- doc_df %>% dplyr::filter(.data$doc_index == rownum) %>% dplyr::mutate( @@ -478,7 +492,7 @@ word_to_md <- function(md_flag = "markdown/", ) ) %>% dplyr::pull(.data$text) - + doc_df <- doc_df %>% dplyr::mutate( text = replace( @@ -487,38 +501,37 @@ word_to_md <- function(md_flag = "markdown/", chyp_applied ) ) - + offset <- offset + increment + nchar(url) } } - + # Numbered lists need special treatment, the XML is very convoluted... numbering_xml <- file.path( doc$package_dir, "word", "numbering.xml" ) %>% xml2::read_xml() - - num_ids_alt_1 <- numbering_xml %>% + + num_ids_alt_1 <- numbering_xml %>% xml2::xml_find_all("//w:num[w:abstractNumId/@w:val='99411']") %>% - xml2::xml_attr("numId") %>% + xml2::xml_attr("numId") %>% as.numeric() - num_ids_alt_2 <- numbering_xml %>% + num_ids_alt_2 <- numbering_xml %>% xml2::xml_find_all("//w:num[w:abstractNumId/@w:val='2']") %>% - xml2::xml_attr("numId") %>% + xml2::xml_attr("numId") %>% as.numeric() num_ids <- c(num_ids_alt_1, num_ids_alt_2) - + # Add heading, bullet and list markup and find where to add blank lines doc_df <- doc_df %>% dplyr::mutate( next_style = dplyr::lead(.data$style_name), blank_after = ( (.data$style_name != "Compact") | - (.data$style_name == "Compact" & - dplyr::lead(.data$style_name) != "Compact") - ) & - (!is.na(dplyr::lead(.data$style_name))), + (.data$style_name == "Compact" & + dplyr::lead(.data$style_name) != "Compact") + ) & (!is.na(dplyr::lead(.data$style_name))), style_name = dplyr::case_when( .data$num_id %in% num_ids ~ "Numbering", TRUE ~ .data$style_name @@ -533,133 +546,33 @@ word_to_md <- function(md_flag = "markdown/", "heading 3" ~ paste0("### ", .data$text), "Heading 4" ~ paste0("#### ", .data$text), "heading 4" ~ paste0("#### ", .data$text), - "Compact" ~ paste0("- ", .data$text), - .default = .data$text + "Compact" ~ paste0("- ", .data$text), + .default = .data$text ) ) %>% dplyr::group_by(.data$num_id) %>% dplyr::mutate( text = dplyr::case_match( .data$style_name, - "Numbering" ~ paste0(seq(dplyr::n()), ". ", .data$text), - .default = .data$text + "Numbering" ~ paste0(seq_len(dplyr::n()), ". ", .data$text), + .default = .data$text ) - ) %>% + ) %>% dplyr::ungroup() - + # Get the element indices which need a blank line afterward needs_blank_after <- which(doc_df$blank_after) + seq_len(length(which(doc_df$blank_after))) - 1 - + # Iterate over the indices and add a new blank row after each purrr::walk( needs_blank_after, \(x) doc_df <<- dplyr::add_row(doc_df, text = "", .after = x) ) - + # Write the markdown file dir.create(md_out_dir, showWarnings = FALSE) writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) } ) } - -# Instructions ------------------------------------------------------------ -# 1. Get initial snapshots -# 2. Comment out the first code block -# 3. Uncomment the rest of the code -# 4. Whenever you make changes to these files, 'Run Tests' again -# 5. Any differences will be caught; run the below code in the console to review -# them in a shiny app (working dir is project root): -# testthat::snapshot_review('snapshot_tests/', "review/tests") -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# If there are some changes that were not intended, you should fix these in -# the temporary markdown files (review/temp). You can leave the diff viewer -# open and revert the changes directly by a simple copy and paste of the -# original content. Click 'Skip' once the reversions are complete. -# 7. Close the diff viewer once only skipped files remain. -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. -# 9. If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, uncomment it now so it is ready for the next set of -# changes. - -# gen_snaps <- function() { -# withr::with_dir(rprojroot::find_package_root_file(), { -# source("review/scripts/md_to_word.R") -# -# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") -# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") -# }) -# } - -# Generate snapshots ------------------------------------------------------ -# 1. To get the initial snapshot, run the below block, with an expectation for -# each reference file. Use 'Run Tests' button above right. -# You are basically saying "all these files should look as they are now in -# future". -# test_that("generate snapshots", { -# with_dir(find_package_root_file(), { -# source("review/scripts/md_to_word.R") -# -# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") -# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") -# }) -# }) - -# 2. Comment the above block out now - -# 3. Uncomment the below block, ready for future changes - - -# Compare markdown -------------------------------------------------------- -# 4. When you make changes to the files, use 'Run Tests' button above right -# test_that("compare markdown", { -# with_dir(find_package_root_file(), { -# source("review/scripts/word_to_md.R") -# -# expect_snapshot_file("review/temp/01_mod_the_first.md") -# expect_snapshot_file("review/temp/02_mod_the_second.md") -# }) -# }) - - -# Run diff viewer --------------------------------------------------------- -# 5. If any differences are present (there should be if you changed the files...) -# a diff viewer can be opened by running the code below in the console (you can -# open it in browser using the 'Show in new window' button in the Viewer pane) -# testthat::snapshot_review('snapshot_tests/', "review/tests") - - -# Review - accept or fix -------------------------------------------------- -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# -# If there are some changes that were not intended, you should fix these in -# the source files. You can leave the diff viewer open and revert the changes -# directly by a simple copy and paste of the original content. -# -# Click 'Skip' once the reversions are complete. - -# 7. Close the diff viewer once only skipped files remain. - -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. - - -# Replace markdown -------------------------------------------------------- -# 9. It is now safe to copy the revised markdown files from the review/temp folder -# over the app markdown files in inst/app/www/assets/markdown. -# -# If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, MAKE SURE to uncomment it now so it is ready for the next -# set of changes. \ No newline at end of file diff --git a/inst/review/tests/snapshot_tests.R b/inst/review/tests/snapshot_tests.R index 30c21c5..0ba500a 100644 --- a/inst/review/tests/snapshot_tests.R +++ b/inst/review/tests/snapshot_tests.R @@ -1,8 +1,9 @@ -library(testthat) -library(withr) -library(rprojroot) - -local_edition(3) +# Begin Exclude Linting +# library(testthat) +# library(withr) +# library(rprojroot) +# +# local_edition(3) # Instructions ------------------------------------------------------------ @@ -15,19 +16,19 @@ local_edition(3) # testthat::snapshot_review('snapshot_tests/', "review/tests") # 6. Review the changes in each file one by one. # If ALL changes in a file are as intended, then choose the 'Accept' option. -# If there are some changes that were not intended, you should fix these in +# If there are some changes that were not intended, you should fix these in # the temporary markdown files (review/temp). You can leave the diff viewer # open and revert the changes directly by a simple copy and paste of the # original content. Click 'Skip' once the reversions are complete. # 7. Close the diff viewer once only skipped files remain. # 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") +# source("review/scripts/word_to_md.R") # then run this test file again with 'Run Tests'. You should expect no # differences to be found now, but if there are then repeat step 6 to 8 until # none are found. # 9. If you had to comment out the line # source("review/scripts/word_to_md.R") -# due to reversions, uncomment it now so it is ready for the next set of +# due to reversions, uncomment it now so it is ready for the next set of # changes. @@ -36,14 +37,14 @@ local_edition(3) # each reference file. Use 'Run Tests' button above right. # You are basically saying "all these files should look as they are now in # future". -test_that("generate snapshots", { - with_dir(find_package_root_file(), { - source("review/scripts/md_to_word.R") - - expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") - expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") - }) -}) +# test_that("generate snapshots", { +# with_dir(find_package_root_file(), { +# source("review/scripts/md_to_word.R") +# +# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") +# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") +# }) +# }) # 2. Comment the above block out now @@ -55,7 +56,7 @@ test_that("generate snapshots", { # test_that("compare markdown", { # with_dir(find_package_root_file(), { # source("review/scripts/word_to_md.R") -# +# # expect_snapshot_file("review/temp/01_mod_the_first.md") # expect_snapshot_file("review/temp/02_mod_the_second.md") # }) @@ -64,7 +65,7 @@ test_that("generate snapshots", { # Run diff viewer --------------------------------------------------------- # 5. If any differences are present (there should be if you changed the files...) -# a diff viewer can be opened by running the code below in the console (you can +# a diff viewer can be opened by running the code below in the console (you can # open it in browser using the 'Show in new window' button in the Viewer pane) # testthat::snapshot_review('snapshot_tests/', "review/tests") @@ -73,7 +74,7 @@ test_that("generate snapshots", { # 6. Review the changes in each file one by one. # If ALL changes in a file are as intended, then choose the 'Accept' option. # -# If there are some changes that were not intended, you should fix these in +# If there are some changes that were not intended, you should fix these in # the source files. You can leave the diff viewer open and revert the changes # directly by a simple copy and paste of the original content. # @@ -82,7 +83,7 @@ test_that("generate snapshots", { # 7. Close the diff viewer once only skipped files remain. # 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") +# source("review/scripts/word_to_md.R") # then run this test file again with 'Run Tests'. You should expect no # differences to be found now, but if there are then repeat step 6 to 8 until # none are found. @@ -96,3 +97,4 @@ test_that("generate snapshots", { # source("review/scripts/word_to_md.R") # due to reversions, MAKE SURE to uncomment it now so it is ready for the next # set of changes. +# End Exclude Linting diff --git a/man/md_to_word.Rd b/man/md_to_word.Rd index 5f96f50..e96b1f7 100644 --- a/man/md_to_word.Rd +++ b/man/md_to_word.Rd @@ -24,7 +24,7 @@ md_to_word( Path of generated Word doc } \description{ -All markdown files in a folder can be used to generate a Word document. The +All markdown files in a folder can be used to generate a Word document. The main use case is for use in reviewing often changing text when writing final output of initiatives. It can be used standalone for adhoc purposes. } diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index 721e9ec..ba8fe79 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -7,27 +7,27 @@ test_that("md_to_word generates expected Word doc", { rv_dir = rv_dir, docx_file = docx_file ) - + # When run for first time, just save the generated Word doc for future use # Note the test will essentially be comparing this doc to itself on that first # run! if (!file.exists(docx_file)) file.copy(docx_path, docx_file) - + # Get docx objects for comparisons old <- officer::read_docx(docx_file) new <- officer::read_docx(docx_path) - + comp_structure <- waldo::compare(new, old) - + # Due to how officer::read_docx works, we expect there to always be # differences in the package_dir and doc_properties$data - if there are more # than 2 diffs, something unexpected is also different. - # If unexpected number of diffs, print out the comparison for ease of seeing + # If unexpected number of diffs, print out the comparison for ease of seeing # where the fail is. if (length(comp_structure) > 2) cat("\n\n", comp_structure, sep = "\n") - + expect_lte(length(comp_structure), 2) - + # Since read_docx only has pointers to the actual text content, also need to # compare the content separately. expect_equal(officer::docx_summary(new), officer::docx_summary(old)) @@ -43,13 +43,13 @@ test_that("word_to_md generates expected markdown files", { rv_dir = rv_dir, docx_file = docx_file ) - + word_to_md( rv_dir = rv_dir, docx_file = docx_file, md_out_dir = md_dir ) - + expect_snapshot_file(file.path(md_dir, "01_md_file.md")) expect_snapshot_file(file.path(md_dir, "02_md_file.md")) }) @@ -65,7 +65,7 @@ test_that("style_map generates expected maps", { docx_file = docx_file ) doc <- officer::read_docx(docx_path) - + style_maps <- list( bold_map = style_map(doc, "r", "rPr", "b"), ital_map = style_map(doc, "r", "rPr", "i"), @@ -73,6 +73,6 @@ test_that("style_map generates expected maps", { code_map = style_map(doc, "r", "rPr", "rStyle", "VerbatimChar"), chyp_map = style_map(doc, "hyperlink", "r", "rPr", NULL, "rStyle", "VerbatimChar") ) - + expect_snapshot(style_maps) -}) \ No newline at end of file +}) From 9b23ffb592f765b22a761f320d3b68274f85ae45 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Fri, 17 Nov 2023 17:22:04 +0000 Subject: [PATCH 20/32] Remove step from GHA, unnecessary --- .github/workflows/R-CMD-check.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 8b21304..a4b4ac3 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -46,6 +46,3 @@ jobs: with: name: ${{ runner.os }}-r${{ matrix.config.r }}-results path: check - - - name: Download all workflow run artifacts - uses: actions/download-artifact@v3 From 2cb1fa139d1e67fa53b1cbce869e1e38537e77d1 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 20 Nov 2023 07:54:28 +0000 Subject: [PATCH 21/32] Use Windows for R CMD check GHA --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a4b4ac3..439e9f4 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -10,7 +10,7 @@ name: R-CMD-check jobs: R-CMD-check: - runs-on: ubuntu-latest + runs-on: windows-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} From d26b647baa4f395962aa5d132c94e90db42514e3 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 20 Nov 2023 08:05:02 +0000 Subject: [PATCH 22/32] Include pandoc in R CMD check GHA --- .github/workflows/R-CMD-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 439e9f4..14acb8c 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -20,6 +20,8 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: r-lib/actions/setup-pandoc@v2 + - uses: r-lib/actions/setup-r@v2 with: use-public-rspm: true From da300a3b6f88040a77025383497ab243566c024d Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 20 Nov 2023 08:26:02 +0000 Subject: [PATCH 23/32] Temporarily save generated Word doc for troubleshooting GHA --- tests/testthat/test-utils_review.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index ba8fe79..c66502d 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -7,6 +7,9 @@ test_that("md_to_word generates expected Word doc", { rv_dir = rv_dir, docx_file = docx_file ) + + dir.create("tmp") + file.copy(basename(docx_path), "tmp") # When run for first time, just save the generated Word doc for future use # Note the test will essentially be comparing this doc to itself on that first From c68d0157e43530217f3f2061fd14a9e94f8945d9 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 20 Nov 2023 08:36:38 +0000 Subject: [PATCH 24/32] Try using Word doc generated by GHA to compare against --- tests/testthat/test-utils_review.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index c66502d..00e0837 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -1,5 +1,8 @@ test_that("md_to_word generates expected Word doc", { - docx_file <- "review.docx" + is_ci <- testthat:::on_ci() + + docx_file <- ifelse(testthat:::on_ci(), "review_ci.docx", "review.docx") + cat( "\n\n", docx_file, "\n\n") md_dir <- local_create_md() rv_dir <- gsub("markdown", "review", md_dir) docx_path <- local_create_word_doc( @@ -7,9 +10,6 @@ test_that("md_to_word generates expected Word doc", { rv_dir = rv_dir, docx_file = docx_file ) - - dir.create("tmp") - file.copy(basename(docx_path), "tmp") # When run for first time, just save the generated Word doc for future use # Note the test will essentially be comparing this doc to itself on that first From 2564a32b233f478a8cd5aa36235f63cd888e45c8 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 20 Nov 2023 10:33:00 +0000 Subject: [PATCH 25/32] Use better starting text for styles doc --- inst/review/styles/draft-styles.rmd | 149 ++++++++++++++++++++--- vignettes/writing-and-reviewing-text.Rmd | 10 +- 2 files changed, 142 insertions(+), 17 deletions(-) diff --git a/inst/review/styles/draft-styles.rmd b/inst/review/styles/draft-styles.rmd index 7bf2b77..1310cc9 100644 --- a/inst/review/styles/draft-styles.rmd +++ b/inst/review/styles/draft-styles.rmd @@ -1,28 +1,145 @@ --- -title: "Untitled" output: word_document --- -```{r setup, include=FALSE} -knitr::opts_chunk$set(echo = TRUE) -``` +markdown/01_markdown.md -## R Markdown +## Markdown cheat sheet -This is an R Markdown document. Markdown is a simple formatting syntax for authoring HTML, PDF, and MS Word documents. For more details on using R Markdown see . +The markdown syntax compatible with the review automation scripts is shown below. There exists further markdown syntax that could be incorporated if required. -When you click the **Knit** button a document will be generated that includes both content as well as the output of any embedded R code chunks within the document. You can embed an R code chunk like this: +### Heading -```{r cars} -summary(cars) -``` +Headings from level one to four can be used. -## Including Plots +# H1 +## H2 +### H3 +#### H4 -You can also embed plots, for example: +### Bold -```{r pressure, echo=FALSE} -plot(pressure) -``` +Original markdown used double * characters, but underscore can be used, and is what is used for the review scripts to work. -Note that the `echo = FALSE` parameter was added to the code chunk to prevent printing of the R code that generated the plot. +__bold text__ + +### Italic + +Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. + +_italicized text_ + +### Ordered list + +Only single level lists are supported currently. + +1. First item +2. Second item +3. Third item + +### Unordered list + +- First item +- Second item +- Third item + +### Inline code + +Inline code is usually made with a single pair of backticks. We need to use four pairs of backticks to allow it to work in the review code. + +Here is some inline `code`. + +### External link + +[Markdown Guide](https://www.markdownguide.org) + +### Internal link + +You can link to another page using the `localhost` IP address and pointing to a page by using the `title` of its `tabpanel`, as defined in `app_ui.R`, with any spaces replaced by underscores. + +[Link to page "Another markdown page"](http://127.0.0.1/Another_markdown_page) + +You can even link to a specific heading on another page. Just add a `?` followed by a string formed from the lower case heading text, with non-alphanumeric characters removed and spaces replaced with dashes. + +[Link to heading "Linked heading" on page "Another markdown page"](http://127.0.0.1/Another_markdown_page?linked-heading) + +The browser back button will also be enabled after using an internal link, to allow you to get back to where you were. + +### Inline code with link + +Links can be created on inline code using + +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) + +Note the braces (`{}`) are not necessary, but are a convention when writing an R package name. + +markdown/02_markdown.md + +## Markdown cheat sheet + +The markdown syntax compatible with the review automation scripts is shown below. There exists further markdown syntax that could be incorporated if required. + +### Heading + +Headings from level one to four can be used. + +# H1 +## H2 +### H3 +#### H4 + +### Bold + +Original markdown used double * characters, but underscore can be used, and is what is used for the review scripts to work. + +__bold text__ + +### Italic + +Original markdown used the * character, but underscore can be used, and is what is used for the review scripts to work. + +_italicized text_ + +### Ordered list + +Only single level lists are supported currently. + +1. First item +2. Second item +3. Third item + +### Unordered list + +- First item +- Second item +- Third item + +### Inline code + +Inline code is usually made with a single pair of backticks. We need to use four pairs of backticks to allow it to work in the review code. + +Here is some inline `code`. + +### External link + +[Markdown Guide](https://www.markdownguide.org) + +### Internal link + +You can link to another page using the `localhost` IP address and pointing to a page by using the `title` of its `tabpanel`, as defined in `app_ui.R`, with any spaces replaced by underscores. + +[Link to page "Another markdown page"](http://127.0.0.1/Another_markdown_page) + +You can even link to a specific heading on another page. Just add a `?` followed by a string formed from the lower case heading text, with non-alphanumeric characters removed and spaces replaced with dashes. + +[Link to heading "Linked heading" on page "Another markdown page"](http://127.0.0.1/Another_markdown_page?linked-heading) + +The browser back button will also be enabled after using an internal link, to allow you to get back to where you were. + +### Inline code with link + +Links can be created on inline code using + +[````{nhsbsaShinyR}````](https://github.com/nhsbsa-data-analytics/nhsbsaShinyR) + +Note the braces (`{}`) are not necessary, but are a convention when writing an R package name. diff --git a/vignettes/writing-and-reviewing-text.Rmd b/vignettes/writing-and-reviewing-text.Rmd index e77b780..64ff10a 100644 --- a/vignettes/writing-and-reviewing-text.Rmd +++ b/vignettes/writing-and-reviewing-text.Rmd @@ -130,7 +130,15 @@ This list is then written as an Rmarkdown document, and finally rendered as a Wo ## Assisted review -Once the text is first added to the app, as markdown files, there is probably going to be a time when something needs to change, based on feedback or a rethink of what is written. This review process can be made easy with the following steps +Once the text is first added to the app, as markdown files, there is probably going to be a time when something needs to change, based on feedback or a rethink of what is written. Using the automation provided by the review functions makes keeping the draft text and the app text synced quicker, easier and less error-prone. + +The end-to-end process follows. + +1. Generate the initial Word document as in ['Word-first' approach](127.0.0.1/#word-first-approach). +2. Write the initial draft of text in Word, using the built-in review functionality to track changes and comments. It is important to use the conventions laid out in ['Word-first' approach](127.0.0.1/#word-first-approach), as without these the functions may give unexpected results. Keep it in Word for as long as possible! +3. When you get to a point when you _have_ to deploy the app with the text, run `word_to_md`. From this point on, the app text and the Word text need to be synced as changes are agreed. With the default arguments, `word_to_md` will place the generated markdown files in `inst/review/temp`. +4. Run `review_md` to get an interactive diff between the generated markdown and the existing markdown, if it exists. If no existing markdown is present in `inst/app/www/assets/markdown`, this will create file snapshots instead. +5. 1. Make sure to base changes on the current state of the app text, by running the `review/md_to_word.R` script first 2. (First review only) To get the initial snapshots, run the first code block (`Generate snapshots`) in `review/tests/snapshot_tests.R`; each markdown file should have an expectation From bb61aca6828f9c85c8601c8375d4359f48e4376c Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Tue, 21 Nov 2023 17:07:45 +0000 Subject: [PATCH 26/32] Most functionality present and tested + vignette almost complete --- DESCRIPTION | 3 +- R/utils_review.R | 209 ++++++++++++++++++++++- inst/review/styles/.gitignore | 1 + inst/review/tests/.gitignore | 1 + inst/review/tests/snapshot_tests.R | 100 ----------- man/word_to_md.Rd | 5 +- tests/testthat/review_ci.docx | Bin 0 -> 13523 bytes tests/testthat/test-utils_review.R | 3 +- vignettes/writing-and-reviewing-text.Rmd | 58 +++---- 9 files changed, 240 insertions(+), 140 deletions(-) create mode 100644 inst/review/styles/.gitignore create mode 100644 inst/review/tests/.gitignore delete mode 100644 inst/review/tests/snapshot_tests.R create mode 100644 tests/testthat/review_ci.docx diff --git a/DESCRIPTION b/DESCRIPTION index 93290e8..ce9e05f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -29,6 +29,7 @@ Depends: R (>= 4.0) Imports: config, + diffviewer, dplyr, golem, highcharter, @@ -43,6 +44,7 @@ Imports: scrollytell, shiny, shinyjs, + testthat (>= 3.0.0), tidyr, utils, withr, @@ -51,7 +53,6 @@ Suggests: knitr, pkgload, markdown, - testthat (>= 3.0.0), usethis, waldo Remotes: diff --git a/R/utils_review.R b/R/utils_review.R index 4f88a53..ade1ee8 100644 --- a/R/utils_review.R +++ b/R/utils_review.R @@ -1,3 +1,15 @@ +gen_review_doc <- function(rv_dir = "inst/review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd") { + styles_doc <- rmarkdown::render( + styles_rmd, + output_dir = rv_dir, + output_file = docx_file, + quiet = TRUE + ) +} + + #' Generate Word doc from markdown files #' #' All markdown files in a folder can be used to generate a Word document. The @@ -166,16 +178,16 @@ style_map <- function(doc, t1, t2 = NULL, if (as.character(i - num_blank) %in% names(smap)) { prev_index <- length(smap[[as.character(i - num_blank)]]) new_index <- prev_index + 1 - if (style_data[[1]] == + if ( + style_data[[1]] == smap[[as.character(i - num_blank)]][[prev_index]][[2]] + 1) { - smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- style_data[[2]] + smap[[as.character(i - num_blank)]][[prev_index]][[2]] <- + style_data[[2]] } else { smap[[as.character(i - num_blank)]][[new_index]] <- style_data } } else { - smap[[as.character(i - num_blank)]] <- list( - `1` = style_data - ) + smap[[as.character(i - num_blank)]] <- list(`1` = style_data) } } } @@ -197,6 +209,7 @@ style_map <- function(doc, t1, t2 = NULL, #' @param rv_dir Review directory #' @param docx_file Output Word document file name #' @param md_out_dir Output folder for markdown files +#' @param first_run_snaps Should snapshot files be generated if there are none? #' #' @return Nothing, used for side effects only #' @export @@ -218,7 +231,8 @@ style_map <- function(doc, t1, t2 = NULL, word_to_md <- function(md_flag = "markdown/", rv_dir = "inst/review", docx_file = "review.docx", - md_out_dir = "inst/review/temp") { + md_out_dir = "inst/review/temp", + first_run_snaps = TRUE) { # End Exclude Linting doc <- officer::read_docx(file.path(rv_dir, docx_file)) doc_df <- officer::docx_summary(doc) @@ -575,4 +589,187 @@ word_to_md <- function(md_flag = "markdown/", writeLines(doc_df$text, file.path(md_out_dir, basename(md_file))) } ) + + # Unless disabled, create initial snapshot files if there is no or an empty + # _snaps dir + if (first_run_snaps) { + if (file.exists(file.path(rv_dir, "_snaps"))) { + if (!length(Sys.glob(file.path(rv_dir, "tests/_snaps/review_md", "*.md")))) { + review_md_dir() + } + + return(invisible()) + } + + review_md_dir() + } + + invisible() +} + + +review_md <- function(path, name = basename(path), snaps_dir = "inst/review/tests") { + withr::local_options(list(warn = 1)) + + snap_dir <- file.path(snaps_dir, "_snaps") + snapshotter <- testthat::local_snapshotter(snap_dir = snap_dir) + snapshotter$start_file("review_md", "test") + + lab <- rlang::quo_label(rlang::enquo(path)) + msg <- utils::capture.output({ + equal <- snapshotter$take_file_snapshot( + name, + path, + file_equal = testthat::compare_file_text, + variant = NULL, + trace_env = rlang::caller_env() + ) + }, + type = "message") + if (!identical(msg, character(0))) { + message(gsub("tests/testthat", snaps_dir, msg)) + } + + tryCatch({ + testthat::expect( + equal, + sprintf( + "Snapshot of %s to \"%s\" has changed\n%s", + lab, + paste0(snap_dir, "/", name), + "Run nhsbsaShinyR::review_md_diff() to review changes" + ) + ) + }, + error = \(e) message(gsub("Error", "Warning", e))) + + snapshotter$end_file() +} + + +review_md_dir <- function(md_dir = "inst/review/temp", snaps_dir = "inst/review/tests") { + md_files <- Sys.glob(file.path(md_dir, "*.md")) + + purrr::walk( + md_files, + \(x) { + testthat::announce_snapshot_file(x) + } + ) + purrr::walk( + md_files, + \(x) { + review_md(x, snaps_dir = snaps_dir) + } + ) +} + + +review_md_diff <- function(files = "review_md/", path = "inst/review/tests") { + rlang::check_installed(c("shiny", "diffviewer"), "to use review_md_diff()") + + changed <- testthat:::snapshot_meta(files, path) + if (nrow(changed) == 0) { + rlang::inform("No snapshots to update") + return(invisible()) + } + + name <- changed$name + old_path <- changed$cur + new_path <- changed$new + + stopifnot( + length(name) == length(old_path), + length(old_path) == length(new_path) + ) + + n <- length(name) + case_index <- stats::setNames(seq_along(name), name) + handled <- rep(FALSE, n) + + ui <- shiny::fluidPage( + style = "margin: 0.5em", + shiny::fluidRow( + style = "display: flex", + shiny::div( + style = "flex: 1 1", + shiny::selectInput("cases", NULL, case_index, width = "100%") + ), + shiny::div( + class = "btn-group", style = "margin-left: 1em; flex: 0 0 auto", + shiny::actionButton("reject", "Reject", class = "btn-danger"), + shiny::actionButton("skip", "Skip", class = "btn-warning"), + shiny::actionButton("accept", "Accept", class = "btn-success") + ), + shiny::div( + class = "btn-group", style = "margin-left: 1em; flex: 0 0 auto", + shiny::actionButton("close", "Close", class = "btn-primary") + ) + ), + shiny::fluidRow( + diffviewer::visual_diff_output("diff") + ) + ) + server <- function(input, output, session) { + i <- shiny::reactive(as.numeric(input$cases)) + output$diff <- diffviewer::visual_diff_render({ + diffviewer::visual_diff(old_path[[i()]], new_path[[i()]]) + }) + + # Handle buttons - after clicking update move input$cases to next case, + # and remove current case (for accept/reject). If no cases left, close app + shiny::observeEvent(input$reject, { + rlang::inform(paste0("Rejecting snapshot: '", new_path[[i()]], "'")) + unlink(new_path[[i()]]) + update_cases() + }) + shiny::observeEvent(input$accept, { + rlang::inform(paste0("Accepting snapshot: '", old_path[[i()]], "'")) + file.rename(new_path[[i()]], old_path[[i()]]) + update_cases() + }) + shiny::observeEvent(input$skip, { + i <- next_case() + shiny::updateSelectInput(session, "cases", selected = i) + }) + shiny::observeEvent(input$close, { + rlang::inform("Review ended with changes still present") + shiny::stopApp() + return() + }) + + update_cases <- function() { + handled[[i()]] <<- TRUE + i <- next_case() + + shiny::updateSelectInput( + session, + "cases", + choices = case_index[!handled], + selected = i + ) + } + next_case <- function() { + if (all(handled)) { + rlang::inform("Review complete") + shiny::stopApp() + return() + } + + # Find next case; + remaining <- case_index[!handled] + next_cases <- which(remaining > i()) + if (length(next_cases) == 0) remaining[[1]] else remaining[[next_cases[[1]]]] + } + } + + rlang::inform(c( + "Starting Shiny app for snapshot review", + i = "Use Ctrl + C to quit" + )) + shiny::runApp( + shiny::shinyApp(ui, server), + quiet = TRUE + ) + invisible() } diff --git a/inst/review/styles/.gitignore b/inst/review/styles/.gitignore new file mode 100644 index 0000000..933f12d --- /dev/null +++ b/inst/review/styles/.gitignore @@ -0,0 +1 @@ +draft-styles.docx diff --git a/inst/review/tests/.gitignore b/inst/review/tests/.gitignore new file mode 100644 index 0000000..3c9abec --- /dev/null +++ b/inst/review/tests/.gitignore @@ -0,0 +1 @@ +/_snaps/ diff --git a/inst/review/tests/snapshot_tests.R b/inst/review/tests/snapshot_tests.R deleted file mode 100644 index 0ba500a..0000000 --- a/inst/review/tests/snapshot_tests.R +++ /dev/null @@ -1,100 +0,0 @@ -# Begin Exclude Linting -# library(testthat) -# library(withr) -# library(rprojroot) -# -# local_edition(3) - - -# Instructions ------------------------------------------------------------ -# 1. Get initial snapshots -# 2. Comment out the first code block -# 3. Uncomment the rest of the code -# 4. Whenever you make changes to these files, 'Run Tests' again -# 5. Any differences will be caught; run the below code in the console to review -# them in a shiny app (working dir is project root): -# testthat::snapshot_review('snapshot_tests/', "review/tests") -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# If there are some changes that were not intended, you should fix these in -# the temporary markdown files (review/temp). You can leave the diff viewer -# open and revert the changes directly by a simple copy and paste of the -# original content. Click 'Skip' once the reversions are complete. -# 7. Close the diff viewer once only skipped files remain. -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. -# 9. If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, uncomment it now so it is ready for the next set of -# changes. - - -# Generate snapshots ------------------------------------------------------ -# 1. To get the initial snapshot, run the below block, with an expectation for -# each reference file. Use 'Run Tests' button above right. -# You are basically saying "all these files should look as they are now in -# future". -# test_that("generate snapshots", { -# with_dir(find_package_root_file(), { -# source("review/scripts/md_to_word.R") -# -# expect_snapshot_file("inst/app/www/assets/markdown/01_mod_the_first.md") -# expect_snapshot_file("inst/app/www/assets/markdown/02_mod_the_second.md") -# }) -# }) - -# 2. Comment the above block out now - -# 3. Uncomment the below block, ready for future changes - - -# Compare markdown -------------------------------------------------------- -# 4. When you make changes to the files, use 'Run Tests' button above right -# test_that("compare markdown", { -# with_dir(find_package_root_file(), { -# source("review/scripts/word_to_md.R") -# -# expect_snapshot_file("review/temp/01_mod_the_first.md") -# expect_snapshot_file("review/temp/02_mod_the_second.md") -# }) -# }) - - -# Run diff viewer --------------------------------------------------------- -# 5. If any differences are present (there should be if you changed the files...) -# a diff viewer can be opened by running the code below in the console (you can -# open it in browser using the 'Show in new window' button in the Viewer pane) -# testthat::snapshot_review('snapshot_tests/', "review/tests") - - -# Review - accept or fix -------------------------------------------------- -# 6. Review the changes in each file one by one. -# If ALL changes in a file are as intended, then choose the 'Accept' option. -# -# If there are some changes that were not intended, you should fix these in -# the source files. You can leave the diff viewer open and revert the changes -# directly by a simple copy and paste of the original content. -# -# Click 'Skip' once the reversions are complete. - -# 7. Close the diff viewer once only skipped files remain. - -# 8. If you had some changes to revert, FIRST MAKE SURE TO COMMENT OUT the line -# source("review/scripts/word_to_md.R") -# then run this test file again with 'Run Tests'. You should expect no -# differences to be found now, but if there are then repeat step 6 to 8 until -# none are found. - - -# Replace markdown -------------------------------------------------------- -# 9. It is now safe to copy the revised markdown files from the review/temp folder -# over the app markdown files in inst/app/www/assets/markdown. -# -# If you had to comment out the line -# source("review/scripts/word_to_md.R") -# due to reversions, MAKE SURE to uncomment it now so it is ready for the next -# set of changes. -# End Exclude Linting diff --git a/man/word_to_md.Rd b/man/word_to_md.Rd index 04ae073..b7b4547 100644 --- a/man/word_to_md.Rd +++ b/man/word_to_md.Rd @@ -8,7 +8,8 @@ word_to_md( md_flag = "markdown/", rv_dir = "inst/review", docx_file = "review.docx", - md_out_dir = "inst/review/temp" + md_out_dir = "inst/review/temp", + first_run_snaps = TRUE ) } \arguments{ @@ -21,6 +22,8 @@ with name as everything after this} \item{docx_file}{Output Word document file name} \item{md_out_dir}{Output folder for markdown files} + +\item{first_run_snaps}{Should snapshot files be generated if there are none?} } \value{ Nothing, used for side effects only diff --git a/tests/testthat/review_ci.docx b/tests/testthat/review_ci.docx new file mode 100644 index 0000000000000000000000000000000000000000..b7c1205b5779d2cfeddab21e26be6527614a5c21 GIT binary patch literal 13523 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 Date: Wed, 22 Nov 2023 13:55:04 +0000 Subject: [PATCH 27/32] Amend test to account for differences between local and CI env --- tests/testthat/review_ci.docx | Bin 13523 -> 0 bytes tests/testthat/test-utils_review.R | 17 ++++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 tests/testthat/review_ci.docx diff --git a/tests/testthat/review_ci.docx b/tests/testthat/review_ci.docx deleted file mode 100644 index b7c1205b5779d2cfeddab21e26be6527614a5c21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13523 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 2) cat("\n\n", comp_structure, sep = "\n") + if (length(comp_structure) > expected_differences) { + cat("\n\n", comp_structure, sep = "\n") + } - expect_lte(length(comp_structure), 2) + expect_lte(length(comp_structure), expected_differences) # Since read_docx only has pointers to the actual text content, also need to # compare the content separately. From c251549c8935dd676d9c074ae0568f19e9287cf3 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Wed, 22 Nov 2023 14:38:13 +0000 Subject: [PATCH 28/32] TEMP: print for verification of testthat:::on_ci --- tests/testthat/test-utils_review.R | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index 1ff869d..936dda5 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -24,6 +24,15 @@ test_that("md_to_word generates expected Word doc", { # Also, when run on github CI, there are small differences in some of the # colours used in the Word doc styles. expected_differences <- ifelse(testthat:::on_ci(), 6, 2) + + # TEMP: print for verification of testthat:::on_ci + cat( + "\n**********testthat:::on_ci()**********\n", + testthat:::on_ci(), + "\n*********expected_differences*********\n", + expected_differences, + "\n**************************************\n" + ) # If unexpected number of diffs, print out the comparison for ease of seeing # where the fail is. From 36992b64622abe88fc7b4d455ae5d27f026a96ed Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Wed, 22 Nov 2023 14:57:12 +0000 Subject: [PATCH 29/32] Raise expected_differences for CI --- tests/testthat/test-utils_review.R | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index 936dda5..35908b9 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -23,17 +23,8 @@ test_that("md_to_word generates expected Word doc", { # differences in the package_dir and doc_properties$data. # Also, when run on github CI, there are small differences in some of the # colours used in the Word doc styles. - expected_differences <- ifelse(testthat:::on_ci(), 6, 2) + expected_differences <- ifelse(testthat:::on_ci(), 8, 2) - # TEMP: print for verification of testthat:::on_ci - cat( - "\n**********testthat:::on_ci()**********\n", - testthat:::on_ci(), - "\n*********expected_differences*********\n", - expected_differences, - "\n**************************************\n" - ) - # If unexpected number of diffs, print out the comparison for ease of seeing # where the fail is. if (length(comp_structure) > expected_differences) { From 729bd880101e4c2092046400a3e9c9d7cfb635df Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Wed, 22 Nov 2023 16:25:24 +0000 Subject: [PATCH 30/32] Everything documented --- NAMESPACE | 5 + R/test-helpers.R | 3 + R/utils_review.R | 163 +++++++++++++++++++++++++++-- man/gen_review_doc.Rd | 42 ++++++++ man/md_to_word.Rd | 4 +- man/review_md.Rd | 29 +++++ man/review_md_diff.Rd | 38 +++++++ man/review_md_dir.Rd | 35 +++++++ man/update_md.Rd | 34 ++++++ tests/testthat/test-utils_review.R | 2 +- 10 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 man/gen_review_doc.Rd create mode 100644 man/review_md.Rd create mode 100644 man/review_md_diff.Rd create mode 100644 man/review_md_dir.Rd create mode 100644 man/update_md.Rd diff --git a/NAMESPACE b/NAMESPACE index 1262516..e877f48 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export("%>%") export(accessible_action_link) export(accessible_radio_buttons) +export(gen_review_doc) export(h1_tabstop) export(h2_tabstop) export(h3_tabstop) @@ -20,7 +21,11 @@ export(nhs_header) export(nhs_navlistPanel) export(nhs_selectInput) export(nhs_selectizeInput) +export(review_md) +export(review_md_diff) +export(review_md_dir) export(run_app) +export(update_md) export(word_to_md) import(shiny) importFrom(golem,activate_js) diff --git a/R/test-helpers.R b/R/test-helpers.R index 0368f53..32955d3 100644 --- a/R/test-helpers.R +++ b/R/test-helpers.R @@ -1,3 +1,6 @@ +# See https://r-pkgs.org/testing-advanced.html for explanation of having these +# internal functions + local_create_md <- function(temp_dir = tempdir(), env = parent.frame()) { md_dir <- file.path(temp_dir, "markdown") dir.create(md_dir, showWarnings = FALSE) diff --git a/R/utils_review.R b/R/utils_review.R index ade1ee8..c97fa8e 100644 --- a/R/utils_review.R +++ b/R/utils_review.R @@ -1,7 +1,34 @@ +#' Generate a initial Word doc to write draft text in +#' +#' The `{officer}` package works best with a Word document generated from +#' Rmarkdown. This is just a wrapper around `rmarkdown::render` to do that, with +#' default arguments matching our package conventions. +#' +#' @param rv_dir Review directory +#' @param docx_file Output Word document file name +#' @param styles_rmd Path to Rmarkdown that creates Word template +#' +#' @return Path to the generated Word document +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' gen_review_doc() +#' +#' # May be times when you want to use outside of the usual use case, e.g. +#' # generate Word doc for adhoc purposes +#' gen_review_doc( +#' "C:/Users/CYPHER/Downloads", +#' "adhoc.docx", +#' system.file( +#' "inst", "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" +#' ) +#' )} gen_review_doc <- function(rv_dir = "inst/review", docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd") { - styles_doc <- rmarkdown::render( + rmarkdown::render( styles_rmd, output_dir = rv_dir, output_file = docx_file, @@ -35,17 +62,18 @@ gen_review_doc <- function(rv_dir = "inst/review", #' "my/adhoc/markdown", #' "C:/Users/CYPHER/Downloads", #' "adhoc.docx", -#' system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") +#' system.file( +#' "inst", "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" +#' ) #' )} md_to_word <- function(md_dir = "inst/app/www/assets/markdown", rv_dir = "inst/review", docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd") { - styles_doc <- rmarkdown::render( - styles_rmd, - output_dir = tempdir(), - output_file = tempfile(), - quiet = TRUE + styles_doc <- gen_review_doc( + rv_dir = tempdir(), + docx_file = tempfile(), + styles_rmd = styles_rmd ) # Get paths of md files @@ -64,6 +92,12 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", all_md <- purrr::reduce(all_md, c) # All content in one vector writeLines(all_md, file.path(tempdir(), "all_md.rmd")) + # Check and rename if existing review.docx is present + docx_path <- file.path(rv_dir, docx_file) + if (file.exists(docx_path)) { + file.rename(docx_path, file.path(rv_dir, paste0("prev-", docx_file))) + } + # Create Word document rmarkdown::render( file.path(tempdir(), "all_md.rmd"), @@ -78,7 +112,7 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", ) # Return path of generated Word doc - file.path(rv_dir, docx_file) + docx_path } @@ -608,13 +642,33 @@ word_to_md <- function(md_flag = "markdown/", } -review_md <- function(path, name = basename(path), snaps_dir = "inst/review/tests") { +#' Compare and create snapshot for a single markdown file +#' +#' Will announce intention to compare/create snapshots for each markdown file, +#' then do so. +#' +#' @param path The path of the markdown file +#' @param snaps_dir The path of the directory containing (or to create in) the +#' _snaps directory +#' +#' @return Nothing, used for side-effects only +#' @export +#' +#' @examples +#' \dontrun{ +#' review_md( +#' path = "my/adhoc/markdown/md_file.md", +#' snaps_dir = "C:/Users/CYPHER/Downloads" +#' ) +#' )} +review_md <- function(path, snaps_dir = "inst/review/tests") { withr::local_options(list(warn = 1)) snap_dir <- file.path(snaps_dir, "_snaps") snapshotter <- testthat::local_snapshotter(snap_dir = snap_dir) snapshotter$start_file("review_md", "test") + name <- basename(path) lab <- rlang::quo_label(rlang::enquo(path)) msg <- utils::capture.output({ equal <- snapshotter$take_file_snapshot( @@ -644,9 +698,36 @@ review_md <- function(path, name = basename(path), snaps_dir = "inst/review/test error = \(e) message(gsub("Error", "Warning", e))) snapshotter$end_file() + + invisible() } +#' Compare and create snapshots for all markdown files in a directory +#' +#' Will announce intention to compare/create snapshots for each markdown file, +#' then do so. +#' +#' @param md_dir The path to the directory of markdown files +#' @param snaps_dir The path of the directory containing (or to create in) the +#' _snaps directory +#' +#' @return Nothing, used for side-effects only +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' review_md_dir() +#' +#' # Can be paired with review_md_diff() for reviewing custom markdown +#' review_md_dir( +#' md_dir = "my/adhoc/markdown", +#' snaps_dir = "C:/Users/CYPHER/Downloads" +#' ) +#' +#' review_md_diff(snaps_dir = "C:/Users/CYPHER/Downloads") +#' )} review_md_dir <- function(md_dir = "inst/review/temp", snaps_dir = "inst/review/tests") { md_files <- Sys.glob(file.path(md_dir, "*.md")) @@ -662,13 +743,42 @@ review_md_dir <- function(md_dir = "inst/review/temp", snaps_dir = "inst/review/ review_md(x, snaps_dir = snaps_dir) } ) + invisible() } -review_md_diff <- function(files = "review_md/", path = "inst/review/tests") { +#' Review changes to markdown files +#' +#' This is a customised combination of `testthat::snapshot_review` and the +#' unexported function `testthat:::review_app`. The purpose is to allow user to +#' see the changes made to markdown for the app, and then approve or reject +#' them. This is repeated, with changes to the markdown, until all changes are +#' accepted. +#' +#' @param snap_dir The name of the directory the snapshots are in +#' @param snaps_dir The path of the directory containing (or to create in) the +#' _snaps directory +#' +#' @return Nothing, used for side-effects only +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' review_md_diff() +#' +#' # Can be paired with review_md_dir() for reviewing custom markdown +#' review_md_dir( +#' md_dir = "my/adhoc/markdown", +#' snaps_dir = "C:/Users/CYPHER/Downloads" +#' ) +#' +#' review_md_diff(snaps_dir = "C:/Users/CYPHER/Downloads") +#' )} +review_md_diff <- function(snap_dir = "review_md/", snaps_dir = "inst/review/tests") { rlang::check_installed(c("shiny", "diffviewer"), "to use review_md_diff()") - changed <- testthat:::snapshot_meta(files, path) + changed <- testthat:::snapshot_meta(snap_dir, snaps_dir) if (nrow(changed) == 0) { rlang::inform("No snapshots to update") return(invisible()) @@ -771,5 +881,36 @@ review_md_diff <- function(files = "review_md/", path = "inst/review/tests") { shiny::shinyApp(ui, server), quiet = TRUE ) + + invisible() +} + + +#' Move all markdown files from one directory to another +#' +#' Convenience function to quickly move all the reviewed new markdown files to +#' the app markdown folder. +#' +#' @param md_tmp_dir The path to the directory of reviewed markdown files +#' @param md_dir The path to the directory for markdown files used by app +#' +#' @return Nothing, used for side-effects only +#' @export +#' +#' @examples +#' \dontrun{ +#' # For standard use case, default args are fine +#' update_md() +#' +#' # Unlikely needed, but can be used to move markdown files between any two folders +#' update_md( +#' md_dir = "my/adhoc/markdown", +#' snaps_dir = "my/final/markdown" +#' )} +update_md <- function(md_tmp_dir = "inst/review/temp", + md_dir = "inst/app/www/assets/markdown") { + md_files <- Sys.glob(file.path(md_tmp_dir, "*.md")) + file.rename(md_files, file.path(md_dir, basename(md_files))) + invisible() } diff --git a/man/gen_review_doc.Rd b/man/gen_review_doc.Rd new file mode 100644 index 0000000..1ed3bee --- /dev/null +++ b/man/gen_review_doc.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{gen_review_doc} +\alias{gen_review_doc} +\title{Generate a initial Word doc to write draft text in} +\usage{ +gen_review_doc( + rv_dir = "inst/review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd" +) +} +\arguments{ +\item{rv_dir}{Review directory} + +\item{docx_file}{Output Word document file name} + +\item{styles_rmd}{Path to Rmarkdown that creates Word template} +} +\value{ +Path to the generated Word document +} +\description{ +The `{officer}` package works best with a Word document generated from +Rmarkdown. This is just a wrapper around `rmarkdown::render` to do that, with +default arguments matching our package conventions. +} +\examples{ +\dontrun{ +# For standard use case, default args are fine +gen_review_doc() + +# May be times when you want to use outside of the usual use case, e.g. +# generate Word doc for adhoc purposes +gen_review_doc( + "C:/Users/CYPHER/Downloads", + "adhoc.docx", + system.file( + "inst", "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" + ) +)} +} diff --git a/man/md_to_word.Rd b/man/md_to_word.Rd index e96b1f7..d854d51 100644 --- a/man/md_to_word.Rd +++ b/man/md_to_word.Rd @@ -39,6 +39,8 @@ md_to_word( "my/adhoc/markdown", "C:/Users/CYPHER/Downloads", "adhoc.docx", - system.file("review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR") + system.file( + "inst", "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" + ) )} } diff --git a/man/review_md.Rd b/man/review_md.Rd new file mode 100644 index 0000000..1d424ba --- /dev/null +++ b/man/review_md.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{review_md} +\alias{review_md} +\title{Compare and create snapshot for a single markdown file} +\usage{ +review_md(path, snaps_dir = "inst/review/tests") +} +\arguments{ +\item{path}{The path of the markdown file} + +\item{snaps_dir}{The path of the directory containing (or to create in) the +_snaps directory} +} +\value{ +Nothing, used for side-effects only +} +\description{ +Will announce intention to compare/create snapshots for each markdown file, +then do so. +} +\examples{ +\dontrun{ +review_md( + path = "my/adhoc/markdown/md_file.md", + snaps_dir = "C:/Users/CYPHER/Downloads" +) +)} +} diff --git a/man/review_md_diff.Rd b/man/review_md_diff.Rd new file mode 100644 index 0000000..8d6751f --- /dev/null +++ b/man/review_md_diff.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{review_md_diff} +\alias{review_md_diff} +\title{Review changes to markdown files} +\usage{ +review_md_diff(snap_dir = "review_md/", snaps_dir = "inst/review/tests") +} +\arguments{ +\item{snap_dir}{The name of the directory the snapshots are in} + +\item{snaps_dir}{The path of the directory containing (or to create in) the +_snaps directory} +} +\value{ +Nothing, used for side-effects only +} +\description{ +This is a customised combination of `testthat::snapshot_review` and the +unexported function `testthat:::review_app`. The purpose is to allow user to +see the changes made to markdown for the app, and then approve or reject +them. This is repeated, with changes to the markdown, until all changes are +accepted. +} +\examples{ +\dontrun{ +# For standard use case, default args are fine +review_md_diff() + +# Can be paired with review_md_dir() for reviewing custom markdown +review_md_dir( + md_dir = "my/adhoc/markdown", + snaps_dir = "C:/Users/CYPHER/Downloads" +) + +review_md_diff(snaps_dir = "C:/Users/CYPHER/Downloads") +)} +} diff --git a/man/review_md_dir.Rd b/man/review_md_dir.Rd new file mode 100644 index 0000000..cf8dcd2 --- /dev/null +++ b/man/review_md_dir.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{review_md_dir} +\alias{review_md_dir} +\title{Compare and create snapshots for all markdown files in a directory} +\usage{ +review_md_dir(md_dir = "inst/review/temp", snaps_dir = "inst/review/tests") +} +\arguments{ +\item{md_dir}{The path to the directory of markdown files} + +\item{snaps_dir}{The path of the directory containing (or to create in) the +_snaps directory} +} +\value{ +Nothing, used for side-effects only +} +\description{ +Will announce intention to compare/create snapshots for each markdown file, +then do so. +} +\examples{ +\dontrun{ +# For standard use case, default args are fine +review_md_dir() + +# Can be paired with review_md_diff() for reviewing custom markdown +review_md_dir( + md_dir = "my/adhoc/markdown", + snaps_dir = "C:/Users/CYPHER/Downloads" +) + +review_md_diff(snaps_dir = "C:/Users/CYPHER/Downloads") +)} +} diff --git a/man/update_md.Rd b/man/update_md.Rd new file mode 100644 index 0000000..155fe7b --- /dev/null +++ b/man/update_md.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_review.R +\name{update_md} +\alias{update_md} +\title{Move all markdown files from one directory to another} +\usage{ +update_md( + md_tmp_dir = "inst/review/temp", + md_dir = "inst/app/www/assets/markdown" +) +} +\arguments{ +\item{md_tmp_dir}{The path to the directory of reviewed markdown files} + +\item{md_dir}{The path to the directory for markdown files used by app} +} +\value{ +Nothing, used for side-effects only +} +\description{ +Convenience function to quickly move all the reviewed new markdown files to +the app markdown folder. +} +\examples{ +\dontrun{ +# For standard use case, default args are fine +update_md() + +# Unlikely needed, but can be used to move markdown files between any two folders +update_md( + md_dir = "my/adhoc/markdown", + snaps_dir = "my/final/markdown" +)} +} diff --git a/tests/testthat/test-utils_review.R b/tests/testthat/test-utils_review.R index 35908b9..7bb999e 100644 --- a/tests/testthat/test-utils_review.R +++ b/tests/testthat/test-utils_review.R @@ -24,7 +24,7 @@ test_that("md_to_word generates expected Word doc", { # Also, when run on github CI, there are small differences in some of the # colours used in the Word doc styles. expected_differences <- ifelse(testthat:::on_ci(), 8, 2) - + # If unexpected number of diffs, print out the comparison for ease of seeing # where the fail is. if (length(comp_structure) > expected_differences) { From be5ff686120c495bad8e5f2c7c001ebe3bb23cb3 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 15 Jan 2024 15:09:32 +0000 Subject: [PATCH 31/32] Updated following code review + update README --- DESCRIPTION | 1 + NAMESPACE | 2 +- R/{test-helpers.R => test_helpers.R} | 2 +- R/{utils-pipe.R => utils_pipe.R} | 0 R/utils_review.R | 21 +++++++++++++------ README.md | 19 +++++++++++++---- inst/review/tests/.gitignore | 1 - ...{gen_review_doc.Rd => gen_template_doc.Rd} | 10 ++++----- man/pipe.Rd | 2 +- vignettes/writing-and-reviewing-text.Rmd | 14 ++++++++----- 10 files changed, 48 insertions(+), 24 deletions(-) rename R/{test-helpers.R => test_helpers.R} (97%) rename R/{utils-pipe.R => utils_pipe.R} (100%) delete mode 100644 inst/review/tests/.gitignore rename man/{gen_review_doc.Rd => gen_template_doc.Rd} (90%) diff --git a/DESCRIPTION b/DESCRIPTION index ce9e05f..9efff0f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,6 +35,7 @@ Imports: highcharter, htmltools, magrittr, + methods, nhsbsaR, officedown, officer, diff --git a/NAMESPACE b/NAMESPACE index e877f48..d039fda 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,7 +3,7 @@ export("%>%") export(accessible_action_link) export(accessible_radio_buttons) -export(gen_review_doc) +export(gen_template_doc) export(h1_tabstop) export(h2_tabstop) export(h3_tabstop) diff --git a/R/test-helpers.R b/R/test_helpers.R similarity index 97% rename from R/test-helpers.R rename to R/test_helpers.R index 32955d3..8425567 100644 --- a/R/test-helpers.R +++ b/R/test_helpers.R @@ -54,7 +54,7 @@ local_create_word_doc <- function(temp_dir = tempdir(), md_dir, rv_dir, docx_fil styles_rmd <- system.file( "review", "styles", "draft-styles.rmd", - package = "nhsbsaShinyR" + package = methods::getPackageName() ) docx_path <- md_to_word( diff --git a/R/utils-pipe.R b/R/utils_pipe.R similarity index 100% rename from R/utils-pipe.R rename to R/utils_pipe.R diff --git a/R/utils_review.R b/R/utils_review.R index c97fa8e..c642e49 100644 --- a/R/utils_review.R +++ b/R/utils_review.R @@ -14,20 +14,20 @@ #' @examples #' \dontrun{ #' # For standard use case, default args are fine -#' gen_review_doc() +#' gen_template_doc() #' #' # May be times when you want to use outside of the usual use case, e.g. #' # generate Word doc for adhoc purposes -#' gen_review_doc( +#' gen_template_doc( #' "C:/Users/CYPHER/Downloads", #' "adhoc.docx", #' system.file( #' "inst", "review", "styles", "draft-styles.rmd", package = "nhsbsaShinyR" #' ) #' )} -gen_review_doc <- function(rv_dir = "inst/review", - docx_file = "review.docx", - styles_rmd = "inst/review/styles/draft-styles.rmd") { +gen_template_doc <- function(rv_dir = "inst/review", + docx_file = "review.docx", + styles_rmd = "inst/review/styles/draft-styles.rmd") { rmarkdown::render( styles_rmd, output_dir = rv_dir, @@ -70,7 +70,7 @@ md_to_word <- function(md_dir = "inst/app/www/assets/markdown", rv_dir = "inst/review", docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd") { - styles_doc <- gen_review_doc( + styles_doc <- gen_template_doc( rv_dir = tempdir(), docx_file = tempfile(), styles_rmd = styles_rmd @@ -630,6 +630,15 @@ word_to_md <- function(md_flag = "markdown/", if (file.exists(file.path(rv_dir, "_snaps"))) { if (!length(Sys.glob(file.path(rv_dir, "tests/_snaps/review_md", "*.md")))) { review_md_dir() + } else { + rlang::inform(c( + paste0( + "Snapshot folder ", + file.path(rv_dir, "tests/_snaps/review_md"), + " is not empty." + ), + i = "Aborting writing snapshot files" + )) } return(invisible()) diff --git a/README.md b/README.md index 6432287..3063b39 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Also included is a sample `data-raw/` script. Much of the data used in NHSBSA da There are also github actions to check that code conforms to `lintr` (https://lintr.r-lib.org/), passes `R CMD check` and to run a `gitleaks` check. +Functionality to make adding and reviewing text of the app is available. Check out the vignette by running `vignette("writing-and-reviewing-text", "nhsbsaShinyR")`. + ## Structure The package is structured as below. See the "Using this template" section for further details of the files. @@ -45,9 +47,14 @@ nhsbsaShinyR | | └── markdown # Markdown documents │ │ ├── css # NHS front-end toolkit and custom CSS │ │ └── js # NHS front-end toolkit and custom JavaScript +│ ├── review # For files related to app text +│ │ └── styles # +| | ├── .gitignore # Ignore the Word doc +| | └── draft-styles.rmd # Template for review Word document │ └── golem-config.yml # Golem file ├── LICENSE.md # Apache -├── man # Package documentation, created automatically by roxygen2 from your initial comment blocks +├── man # Package documentation, created automatically by roxygen2 from your +| # initial comment blocks ├── NAMESPACE # Automatically generated documentation by roxygen2 ├── nhsbsaShinyR.Rproj # R Project file ├── R # R code for the dashboard @@ -55,13 +62,16 @@ nhsbsaShinyR │ ├── app_config.R # Golem file │ ├── app_server.R # Server component │ ├── app_ui.R # UI component +│ ├── faithful.R # Example of documenting a dataset │ ├── golem_utils_ui.R # Useful utility functions for UI │ ├── mod_*.R # Example Shiny modules | ├── nhs_*.R # NHS styled UI components | ├── nhsbsaShinyR.R # Package documentation file │ ├── run_app.R # Golem file (no modification) +│ ├── test_helpers.R # Supporting functions for tests │ ├── utils_accessibility.R # Custom NHSBSA highcharter theme -│ └── utils-pipe.R # Magrittr %>% operator +│ ├── utils_review.R # Functions used when adding and reviewing text +│ └── utils_pipe.R # {magrittr} %>% operator └── README.md # Brief overview of the package ``` @@ -76,7 +86,7 @@ Under "Repository template" select "nhsbsa-data-analytics/nhsbsaShinyR". Enter a new name and a brief description. -Leave it set as a Private repo for now. Later, when you are finished, consider if you are able to make it public. +Leave it set as a private repo for now. Later, when you are finished, consider if you are able to make it public. ### Renaming the golem app @@ -142,6 +152,8 @@ The `golem-config.yml` file will need to have the `golem_name` field set to your The `app/www` folder contains sub-folders for static assets and CSS and JavaScript files. Add any custom CSS to `css/style.css` and custom JavaScript to `js/custom.js`. +The `review` folder contains only an Rmarkdown file used as a template for generating a Word document that will be used for drafting any text to be used in the app. The folder will be populated with various files and folders used in the review cycle. + ### LICENSE.md Set as Apache PL v2. Can remove this and the `LICENSE` field in `DESCRIPTION` if you want to use another license, using one of the license functions in `usethis`. @@ -165,4 +177,3 @@ All code that is used internally and/or is to be exported from the package belon ### `README.md` Give an overview of your package including how to install and the simplest example of usage. - diff --git a/inst/review/tests/.gitignore b/inst/review/tests/.gitignore deleted file mode 100644 index 3c9abec..0000000 --- a/inst/review/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/_snaps/ diff --git a/man/gen_review_doc.Rd b/man/gen_template_doc.Rd similarity index 90% rename from man/gen_review_doc.Rd rename to man/gen_template_doc.Rd index 1ed3bee..ee5a425 100644 --- a/man/gen_review_doc.Rd +++ b/man/gen_template_doc.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils_review.R -\name{gen_review_doc} -\alias{gen_review_doc} +\name{gen_template_doc} +\alias{gen_template_doc} \title{Generate a initial Word doc to write draft text in} \usage{ -gen_review_doc( +gen_template_doc( rv_dir = "inst/review", docx_file = "review.docx", styles_rmd = "inst/review/styles/draft-styles.rmd" @@ -28,11 +28,11 @@ default arguments matching our package conventions. \examples{ \dontrun{ # For standard use case, default args are fine -gen_review_doc() +gen_template_doc() # May be times when you want to use outside of the usual use case, e.g. # generate Word doc for adhoc purposes -gen_review_doc( +gen_template_doc( "C:/Users/CYPHER/Downloads", "adhoc.docx", system.file( diff --git a/man/pipe.Rd b/man/pipe.Rd index 1f8f237..696a0d3 100644 --- a/man/pipe.Rd +++ b/man/pipe.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils-pipe.R +% Please edit documentation in R/utils_pipe.R \name{\%>\%} \alias{\%>\%} \title{Pipe operator} diff --git a/vignettes/writing-and-reviewing-text.Rmd b/vignettes/writing-and-reviewing-text.Rmd index d94127b..88eb8ab 100644 --- a/vignettes/writing-and-reviewing-text.Rmd +++ b/vignettes/writing-and-reviewing-text.Rmd @@ -28,7 +28,7 @@ The text markdown lives in the `inst/app/www/assets/markdown` folder. list.files("../inst/app/www/assets/markdown") ``` -Usually there is a single file for each module of the app. This is because in general all the text is first in the module, and followed by the graphical components. Sometimes that is not the case. For example, in the Care Homes update a [module for metrics](https://github.com/nhsbsa-data-analytics/care-home-prescribing-2020-2023/blob/main/R/mod_09_metrics.R) includes several tables interspersed in the text. We did try out using markdown created tables, but they did not look great. In the end we created them with `kable`, and use multiple markdown files. The code is below as an example, with most table code omitted for brevity. +Usually there is a single file for each module of the app. This is because in general all the text is first in the module, and followed by the graphical components. Sometimes that is not the case. For example, in the Care Homes update a [module for metrics](https://github.com/nhsbsa-data-analytics/care-home-prescribing-2020-2023/blob/main/R/mod_10_metrics.R) includes several tables interspersed in the text. We did try out using markdown created tables, but they did not look great. In the end we created them with `kable`, and use multiple markdown files. The code is below as an example, with most table code omitted for brevity. ```{r, eval=FALSE} mod_09_metrics_ui <- function(id) { @@ -85,7 +85,9 @@ We can make the process easier and relatively pain-free by standardising our pro The idea here is to use Word, with tracking of changes, for as long as possible. Add the text to the app only once you are certain that the text is finalised, or you are forced to add it in order to publish the app (e.g. for review by 3rd parties). -You will need to use a template Word doc in order to use the automation script. This can be generated by knitting `review/styles/draft-styles.Rmd`. The reason to use this is that the `{officer}` package used in the automation works best with a Word doc that was actually created from Rmd in the first place. There is some scope to redefine some styling if it becomes necessary. This could be pre-knitted and ready to use. However, the risk is that a change is made that impacts the template, but an old version of the template is used. So for the sake of ~15 seconds to find and knit the Rmarkdown to Word it is better to always generate this from scratch. +You will need to use a template Word doc in order to use the automation script. This can be generated by knitting `review/styles/draft-styles.Rmd`. The reason to use this is that the `{officer}` package used in the automation works best with a Word doc that was actually created from Rmd in the first place. There is some scope to redefine some styling if it becomes necessary. + +The template could be pre-knitted and ready to use. However, the risk is that a change is made that impacts the template, but an old version of the template is used. In addition to using the template document when writing the first draft, there are some conventions that need to be followed on which the script depends. All of the (currently) usable syntax is shown on the _Markdown cheat sheet_ page in the example app. @@ -128,11 +130,13 @@ This list is then written as an Rmarkdown document, and finally rendered as a Wo Once the text is first added to the app, as markdown files, there is probably going to be a time when something needs to change, based on feedback or a rethink of what is written. Using the automation provided by the review functions makes keeping the draft text and the app text synced quicker, easier and less error-prone. -The end-to-end process follows. +When starting from scratch, with no text yet written, the end-to-end process follows. If you already have text written in markdown format (ensure to use the conventions as laid out in the _Markdown cheat sheet_ page in the example app), you can generate the Word version of it following step 8. + +The review cycle comprises steps 4 to 8. -1. Generate the initial Word document by running `gen_review_doc()`. The Word doc will be saved in the `inst/review` folder as `review.docx`. +1. Generate the initial Word document by running `gen_template_doc()`. The Word doc will be saved in the `inst/review` folder as `review.docx`. 2. Write the initial draft of text in `review.docx`, using Word's review functionality to track changes and comments. It is important to use the conventions laid out in ['Word-first' approach](127.0.0.1/#word-first-approach), as without these the functions may give unexpected results. Keep it in Word for as long as possible! -3. When you get to a point when you _have_ to deploy the app with the text, run `word_to_md()`. With the default arguments, the generated markdown files are saved to `inst/review/temp`. From this point on, the app text and the Word text need to be synced as changes are agreed. On the first run of `word_to_md` the initial snapshot files will be automatically created in `inst/review/tests/_snaps/review_md`. These snapshots will be used to compare future changes to, and can be updated as the text changes and is added to the app. Note that if any existing markdown is in `inst/review/tests/_snaps/review_md` folder, the initial snaps will not be created, so ensure the folder is either not present or empty. +3. When you get to a point when you _have_ to deploy the app with the text, run `word_to_md()`. With the default arguments, the generated markdown files are saved to `inst/review/temp`. From this point on, the app text and the Word text need to be synced as changes are agreed. On the first run of `word_to_md` the initial snapshot files will be automatically created in `inst/review/tests/_snaps/review_md`. These snapshots will be used to compare future changes to, and can be updated as the text changes and is added to the app. Note that, by default, if any existing markdown is in `inst/review/tests/_snaps/review_md` folder, the initial snaps will not be created, so ensure the folder is either not present or empty. You will see a warning if unable to write the snapshot files due to pre-existing snapshots. 4. If the text now never changes, you are lucky! But usually there will be multiple rounds of adjustments before the final text is agreed. After the previous step, the text in `review.docx` precisely mirrors what is in the app. Continue to use `review.docx` as your _staging area_ (similar to how git has the `add` command to stage changes). Whenever you want to _commit_ (in git terminology, again) changes to the app run `word_to_md()` again, but follow it with a run of `review_md_dir()`. When the latter is run, it will compare the newly generated markdown against the previously saved snapshots. 5. You should now review the new versions of the markdown files. Reviewing them carefully in this way makes it much less likely that something unintended is being changed, such as typos and misspellings. Additionally, if you expected some change but it has not been included, then this is another opportunity to catch the omission. Running `review_md_diff()` will open an interactive diff. This allows to reject, skip or accept a change. Currently each decision is on a per file basis. - Reject From 2b96291280a6d035f05d55318b37d65c35b4f631 Mon Sep 17 00:00:00 2001 From: Mark McPherson Date: Mon, 15 Jan 2024 15:45:53 +0000 Subject: [PATCH 32/32] Add section on installing from repo to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3063b39..b972db6 100644 --- a/README.md +++ b/README.md @@ -177,3 +177,9 @@ All code that is used internally and/or is to be exported from the package belon ### `README.md` Give an overview of your package including how to install and the simplest example of usage. + +## Installing from GitHub + +To install this package from GitHub, use the below code. Note that you must explicitly ask for vignettes to be built when installing from GitHub. + +`devtools::install_github("nhsbsa-data-analytics/nhsbsaShinyR", build_vignettes = TRUE)`