diff --git a/.Rbuildignore b/.Rbuildignore index 7ff8da5..060a29f 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,4 +1,4 @@ -^noclocksR\.Rproj$ +^noclocksr\.Rproj$ ^\.Rproj\.user$ ^cliff\.toml$ ^CHANGELOG\.md$ diff --git a/.github/actions/dependencies/action.yml b/.github/actions/dependencies/action.yml new file mode 100644 index 0000000..3b8eb57 --- /dev/null +++ b/.github/actions/dependencies/action.yml @@ -0,0 +1,9 @@ +name: Dependencies + +runs: + using: 'composite' + steps: + - name: Install package dependencies 📄 + run: | + pak::local_install_deps(".", upgrade=FALSE, ask=FALSE, dependencies = TRUE) + shell: Rscript {0} diff --git a/.github/cliff.toml b/.github/cliff.toml index 2ce9f34..bdef9f4 100644 --- a/.github/cliff.toml +++ b/.github/cliff.toml @@ -1,64 +1,120 @@ -# configuration file for git-cliff -# see for default -# see also: +# No Clocks, LLC Custom Configuration Template File for `git-cliff` + [changelog] + +# remove the leading and trailing whitespace from the template +trim = true + +# header header = """ -# Changelog -*All notable changes to this project will be documented in this file.*\n +# Changelog\n +> All notable changes to this project will be documented in this file. The format is based on +[Keep a Changelog](http://keepachangelog.com/) and this project adheres to +[Semantic Versioning](http://semver.org/).\n """ + +# body - see https://tera.netlify.app/docs/#introduction body = """ {% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + ## [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + (REPOSITORY_URL/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + (REPOSITORY_URL/tree/{{ version }})\ + {% endif %}\ + {% endif %}\ + - ({{ timestamp | date(format="%Y-%m-%d") }}) {% else %}\ ## [Unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ - {% endfor %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {% if '```' in commit.message %}\ + {{ commit.message | upper_first }}\n\ + ([{{ commit.id | truncate(length=7, end="") }}](REPOSITORY_URL/commit/{{ commit.id }})) - ({{ commit.author.name }})\ + {% else %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](REPOSITORY_URL/commit/{{ commit.id }})) - ({{ commit.author.name }})\ + {% endif %}\ + {% if commit.breaking %}\ + {% for breakingChange in commit.footers %}\ + \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} {% endfor %}\n """ -trim = true + footer = """ *** *Changelog generated by [git-cliff](https://github.com/orhun/git-cliff).* +*** """ + [git] conventional_commits = true filter_unconventional = true -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^bug", group = "Bug Fixes"}, - { message = "^doc", group = "Documentation"}, - { message = "^docs", group = "Documentation"}, - { message = "^perf", group = "Performance"}, - { message = "^app", group = "Application"}, - { message = "^api", group = "API"}, - { message = "^data", group = "Data"}, - { message = "^db", group = "Database"}, - { message = "^refactor", group = "Refactoring"}, - { message = "^style", group = "Styling"}, - { message = "^test", group = "Testing"}, - { message = "^setup", group = "Infrastructure"}, - { message = "^infra", group = "Infrastructure"}, - { message = "^meta", group = "Meta"}, - { message = "^config", group = "Configuration"}, - { message = "^design", group = "Design"}, - { message = "^clean", group = "Cleanup"}, - { message = "^unit", group = "Testing"}, - { message = "^enhance", group = "Features"}, - { message = "^cicd", group = "DevOps"}, - { message = "^config", group = "Configuration"}, - { message = "^deploy", group = "Deployment"}, - { message = "^chore\\(release\\): prepare for", skip = true}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -filter_commits = false +filter_commits = true tag_pattern = "v[0-9]*" -skip_tags = "v0.1.0-beta.1" +skip_tags = "v0.0.0.9999" # "v0.1.0-beta.1" ignore_tags = "" -topo_order = false -sort_commits = "oldest" +date_order = true +topo_order = true +sort_commits = "newest" # "oldest" +split_commits = false +protect_breaking_commits = true +# limit_commits = 42 +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](REPOSITORY_URL/issues/${2}))"}, + { pattern = "Merge pull request #([0-9]+) from [^ ]+", replace = "PR [#${1}](REPOSITORY_URL/pull/${1}):"}, +] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^bug", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^docs", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^app", group = "Application" }, + { message = "^api", group = "API" }, + { message = "^data", group = "Data" }, + { message = "^db", group = "Database" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^setup", group = "Setup" }, + { message = "^infra", group = "Infrastructure" }, + { message = "^meta", group = "Meta" }, + { message = "^config", group = "Configuration" }, + { message = "^design", group = "Design" }, + { message = "^clean", group = "Cleanup" }, + { message = "^unit", group = "Testing" }, + { message = "^enhance", group = "Features" }, + { message = "^cicd", group = "DevOps" }, + { message = "^config", group = "Configuration" }, + { message = "^deploy", group = "Deployment" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore", group = "Miscellaneous Tasks", skip = true }, + { body = ".*security", group = "Security" }, +] + + +# ------------------------------------------------------------------------------ +# parse the commits based on https://www.conventionalcommits.org +# filter out the commits that are not conventional +# process each line of a commit as an individual commit +# regex for preprocessing the commit messages +# regex for parsing and grouping commits +# protect breaking changes from being skipped due to matching a skipping commit_parser +# filter out the commits that are not matched by commit parsers +# glob pattern for matching git tags +# regex for skipping tags +# regex for ignoring tags +# sort the tags chronologically +# sort the commits inside sections by oldest/newest order +# limit the number of commits included in the changelog +# ------------------------------------------------------------------------------ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..27a5243 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + assignees: + - "jimbrig" diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 83ad6f4..7e15b64 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,13 +1,13 @@ -name: Generate CHANGELOG.md +name: Generate Changelog on: workflow_dispatch: + workflow_call: push: - branches: - - main - - develop + branches: [ "main" ] + pull_request: jobs: changelog: - name: Generate changelog + name: Generate Changelog runs-on: ubuntu-latest steps: - name: Checkout @@ -15,20 +15,24 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate a changelog - uses: orhun/git-cliff-action@v1 + - name: Run Git Cliff + uses: tj-actions/git-cliff@v1.5.0 id: git-cliff with: - config: ./.github/cliff.toml - args: --verbose - env: - OUTPUT: ./CHANGELOG.md - - - name: Print the changelog - run: cat "${{ steps.git-cliff.outputs.changelog }}" - - - name: Commit and Push Changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} + args: "--verbose" + output: "CHANGELOG.md" + template-config: "./.github/cliff.toml" + - name: Print Changelog + id: print-changelog + run: | + cat "CHANGELOG.md" + # Commit and push the updated changelog, IF not a pull request + - name: Commit and Push Changelog + if: github.event_name != 'pull_request' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add CHANGELOG.md + git commit -m "[chore]: update changelog" + git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git "main" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..49e8333 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +name: Check đŸ“Ļ + +on: + workflow_call: + +concurrency: + group: check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check: + name: ${{ vars.CI_IMAGE }} + runs-on: ubuntu-latest + container: + image: ${{ vars.CI_IMAGE }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout project âŦ‡ī¸ + uses: actions/checkout@v4 + + - name: Install package dependencies 📄 + uses: noclocks/noclocksr/.github/actions/dependencies@main + + - name: Check đŸ“Ļ + run: | + options(crayon.enabled = TRUE) + rcmdcheck::rcmdcheck(error_on = "error", args = "--no-tests") + shell: Rscript {0} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..75db95a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,62 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: Test Coverage + +permissions: read-all + +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr, any::xml2 + needs: coverage + + - name: Test coverage + run: | + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + covr::to_cobertura(cov) + shell: Rscript {0} + + - uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} + file: ./cobertura.xml + plugin: noop + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml new file mode 100644 index 0000000..cd59734 --- /dev/null +++ b/.github/workflows/document.yml @@ -0,0 +1,47 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + paths: ["R/**"] + +name: Document (Roxygen) + +permissions: read-all + +jobs: + document: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::roxygen2 + needs: roxygen2 + + - name: Document + run: roxygen2::roxygenise() + shell: Rscript {0} + + - name: Commit and push changes + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add man/\* NAMESPACE DESCRIPTION + git commit -m "Update documentation" || echo "No changes to commit" + git pull --ff-only + git push origin diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..905b748 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: Lint + +permissions: read-all + +jobs: + lint: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::lintr, local::. + needs: lint + + - name: Lint + run: lintr::lint_package() + shell: Rscript {0} + env: + LINTR_ERROR_ON_LINT: true diff --git a/.github/workflows/news.yml b/.github/workflows/news.yml new file mode 100644 index 0000000..d600923 --- /dev/null +++ b/.github/workflows/news.yml @@ -0,0 +1,52 @@ +name: Generate NEWS.md + +on: + push: + branches: + - main + - develop + workflow_dispatch: + +permissions: read-all + +jobs: + generate_changelog: + uses: ./.github/workflows/changelog.yml + generate_news: + needs: [generate_changelog] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install Dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packges: any::pkgload, any::markdown, any::xml2, any::stringr + needs: pkgload + + - name: Generate NEWS.md + run: | + Rscript -e 'pkgload::load_all(); noclocksr::generate_news(output_file = "NEWS.md", input_file = "CHANGELOG.md")' + + - name: Commit and push changes + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add NEWS.md + git commit -m "docs: Update NEWS.md" || echo "No changes to commit" + git pull --ff-only + git push origin diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml new file mode 100644 index 0000000..bc23a1e --- /dev/null +++ b/.github/workflows/pull-requests.yml @@ -0,0 +1,85 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + issue_comment: + types: [created] + +name: Pull Request Commands + +permissions: read-all + +jobs: + document: + if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} + name: document + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::roxygen2 + needs: pr-document + + - name: Document + run: roxygen2::roxygenise() + shell: Rscript {0} + + - name: commit + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add man/\* NAMESPACE + git commit -m 'Document' + + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + style: + if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} + name: style + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-r@v2 + + - name: Install dependencies + run: install.packages("styler") + shell: Rscript {0} + + - name: Style + run: styler::style_pkg() + shell: Rscript {0} + + - name: commit + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add \*.R + git commit -m 'Style' + + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..94f4241 --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,78 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + paths: ["**.[rR]", "**.[qrR]md", "**.[rR]markdown", "**.[rR]nw", "**.[rR]profile"] + +name: Style + +permissions: read-all + +jobs: + style: + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::styler, any::roxygen2 + needs: styler + + - name: Enable styler cache + run: styler::cache_activate() + shell: Rscript {0} + + - name: Determine cache location + id: styler-location + run: | + cat( + "location=", + styler::cache_info(format = "tabular")$location, + "\n", + file = Sys.getenv("GITHUB_OUTPUT"), + append = TRUE, + sep = "" + ) + shell: Rscript {0} + + - name: Cache styler + uses: actions/cache@v4 + with: + path: ${{ steps.styler-location.outputs.location }} + key: ${{ runner.os }}-styler-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-styler- + ${{ runner.os }}- + + - name: Style + run: styler::style_pkg() + shell: Rscript {0} + + - name: Commit and push changes + run: | + if FILES_TO_COMMIT=($(git diff-index --name-only ${{ github.sha }} \ + | egrep --ignore-case '\.(R|[qR]md|Rmarkdown|Rnw|Rprofile)$')) + then + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git commit ${FILES_TO_COMMIT[*]} -m "Style code (GHA)" + git pull --ff-only + git push origin + else + echo "No changes to commit." + fi diff --git a/.gitignore b/.gitignore index f805bdb..b6ef6e8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ docs token.rds config.yml + +dev/secret.R diff --git a/DESCRIPTION b/DESCRIPTION index 47601fd..2b32df6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,16 +1,17 @@ -Package: noclocksR -Title: What the Package Does (One Line, Title Case) +Package: noclocksr +Title: Internal Development at No Clocks, LLC Version: 0.0.0.9000 Authors@R: person("Jimmy", "Briggs", , "jimmy.briggs@jimbrig.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-7489-8787")) Description: No Clocks, LLC packaged assets and workflows License: MIT + file LICENSE -URL: https://noclocks.github.io/noclocksR/, - https://docs.noclocks.dev/noclocksR/ +URL: https://noclocks.github.io/noclocksr/, + https://docs.noclocks.dev/noclocksr/ Depends: - R (>= 2.10) + R (>= 4.1) Imports: + assertthat, cli, config, covr, @@ -21,36 +22,45 @@ Imports: gargle, gitdown, glue, + grDevices, here, htmltools, httr, httr2, jsonlite, keyring, + lubridate, markdown, metathis, + monochromeR, + paletter, parallel, + pdftools, pkgdown, pkgload, purrr, rlang, + slackr, stringr, testdown, tibble, tibblify, tidyr, + togglr, + usethis, utils, - xml2 + xml2, + yaml Suggests: knitr, rmarkdown, - testthat (>= 3.0.0) + testthat VignetteBuilder: knitr -Config/Needs/website: noclocks/noclocksR +Config/Needs/website: noclocks/noclocksr Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 233e271..8b353f6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,23 +9,52 @@ export(color_palette) export(detectCores) export(digest) export(document_dataset) +export(download_brand_logos) export(download_logo) export(entrace) export(error_cnd) +export(extract_pdf_content) export(fetch_brand) +export(generate_news) +export(generate_news_from_changelog) +export(get_brand_logos) export(get_favicon) export(get_gitignore) export(get_logo_file_name) +export(get_tracked_time) +export(git_attributes) +export(git_config) +export(git_ignore) +export(hex_to_rgb) +export(parse_pdf_content) +export(process_pdfs) +export(rgb_to_hex) +export(rgba_to_hex) export(shiny_resume_body) export(shiny_resume_navbar) export(shiny_resume_page) +export(start_time_tracking) +export(stop_time_tracking) export(trace_back) export(tree) export(typography) +export(use_github_action_news) export(use_noclocks_meta) +export(write_log) import(htmltools) import(pkgdown) +importFrom(assertthat,assert_that) importFrom(cli,ansi_strip) +importFrom(cli,cat_line) +importFrom(cli,cli_abort) +importFrom(cli,cli_alert_info) +importFrom(cli,cli_alert_success) +importFrom(cli,cli_alert_warning) +importFrom(cli,cli_bullets) +importFrom(cli,cli_progress_bar) +importFrom(cli,cli_progress_done) +importFrom(cli,cli_progress_update) +importFrom(cli,cli_warn) importFrom(cli,tree) importFrom(config,get) importFrom(covr,package_coverage) @@ -36,8 +65,14 @@ importFrom(dplyr,mutate) importFrom(dplyr,pull) importFrom(fs,dir_create) importFrom(fs,dir_exists) +importFrom(fs,dir_info) +importFrom(fs,dir_ls) +importFrom(fs,file_create) +importFrom(fs,file_exists) importFrom(fs,file_move) importFrom(fs,path) +importFrom(fs,path_dir) +importFrom(fs,path_ext) importFrom(fs,path_package) importFrom(gargle,gargle_oauth_client_from_json) importFrom(gargle,init_AuthState) @@ -57,22 +92,34 @@ importFrom(httr2,req_url_path_append) importFrom(httr2,request) importFrom(httr2,resp_body_json) importFrom(jsonlite,fromJSON) +importFrom(lubridate,weeks) importFrom(markdown,markdownToHTML) importFrom(metathis,meta) importFrom(metathis,meta_social) importFrom(parallel,detectCores) +importFrom(pdftools,pdf_data) +importFrom(pdftools,pdf_text) importFrom(pkgdown,build_site) importFrom(pkgload,pkg_name) +importFrom(purrr,map) importFrom(purrr,map_chr) importFrom(purrr,pluck) importFrom(purrr,pmap_chr) +importFrom(purrr,pwalk) importFrom(rlang,abort) importFrom(rlang,cnd_entrace) +importFrom(rlang,current_env) importFrom(rlang,entrace) importFrom(rlang,error_cnd) importFrom(rlang,trace_back) +importFrom(stringr,boundary) +importFrom(stringr,str_detect) +importFrom(stringr,str_extract) +importFrom(stringr,str_match) +importFrom(stringr,str_replace) importFrom(stringr,str_replace_all) importFrom(stringr,str_to_lower) +importFrom(stringr,str_trim) importFrom(testdown,test_down) importFrom(tibble,tibble) importFrom(tibblify,tib_chr) @@ -84,4 +131,16 @@ importFrom(tibblify,tib_unspecified) importFrom(tibblify,tibblify) importFrom(tibblify,tspec_object) importFrom(tidyr,unnest) +importFrom(togglr,get_time_entries) +importFrom(togglr,get_toggl_api_token) +importFrom(togglr,set_toggl_api_token) +importFrom(togglr,toggl_start) +importFrom(togglr,toggl_stop) +importFrom(usethis,use_news_md) importFrom(utils,assignInMyNamespace) +importFrom(xml2,read_html) +importFrom(xml2,xml_children) +importFrom(xml2,xml_find_all) +importFrom(xml2,xml_find_first) +importFrom(xml2,xml_name) +importFrom(xml2,xml_text) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..79e1610 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +# noclocksr (development version) + +* Initial CRAN submission. diff --git a/R/brandfetch.R b/R/brandfetch.R index c338d62..68c1939 100644 --- a/R/brandfetch.R +++ b/R/brandfetch.R @@ -51,148 +51,391 @@ fetch_brand <- function( ) res <- req |> httr2::req_perform() + if (res$status_code != 200) { rlang::abort("Brandfetch API request failed") } + + # extract content --------------------------------------------------------- content <- res |> httr2::resp_body_json() - spec <- tibblify::tspec_object( + init_spec <- tibblify::tspec_row( tibblify::tib_chr("id"), tibblify::tib_chr("name"), tibblify::tib_chr("domain"), - tibblify::tib_lgl("claimed"), - tibblify::tib_chr("description"), - tibblify::tib_chr("longDescription"), - tibblify::tib_dbl("qualityScore"), - tibblify::tib_unspecified("images"), - - tibblify::tib_df( - "links", - tibblify::tib_chr("name"), - tibblify::tib_chr("url") - ), - - tibblify::tib_df( - "logos", - tibblify::tib_chr("theme"), - tibblify::tib_df( - "formats", - tibblify::tib_chr("src"), - tibblify::tib_unspecified("background"), - tibblify::tib_chr("format"), - tibblify::tib_int("height"), - tibblify::tib_int("width"), - tibblify::tib_int("size"), + tag_line = tibblify::tib_chr("description"), + description = tibblify::tib_chr("longDescription"), + quality_score = tibblify::tib_dbl("qualityScore") + ) + + brand_init <- tibblify::tibblify(content, init_spec) + + # links ------------------------------------------------------------------- + link_names <- content$links |> purrr::map_chr(purrr::pluck, "name") + link_urls <- content$links |> purrr::map_chr(purrr::pluck, "url") + + links <- tibble::tibble( + name = link_names, + url = link_urls + ) + + # logos ------------------------------------------------------------------- + logo_themes <- c() + logo_types <- c() + logo_urls <- c() + logo_backgrounds <- c() + logo_exts <- c() + logo_heights <- c() + logo_widths <- c() + logo_sizes <- c() + + logo_formats <- content$logos |> purrr::map(purrr::pluck, "formats") + + for (i in seq_along(content$logos)) { + + theme <- content$logos[[i]]$theme + type <- content$logos[[i]]$type + + formats <- content$logos[[i]]$formats + + for (j in seq_along(formats)) { + + url <- formats[[j]]$src + bg <- formats[[j]]$background + ext <- formats[[j]]$format + height <- formats[[j]]$height + width <- formats[[j]]$width + size <- formats[[j]]$size + + if (is.null(bg)) { bg <- NA_character_ } + if (ext == "svg") { + height <- NA_integer_ + width <- NA_integer_ + } + + logo_themes <- c(logo_themes, theme) + logo_types <- c(logo_types, type) + logo_urls <- c(logo_urls, url) + logo_backgrounds <- c(logo_backgrounds, bg) + logo_exts <- c(logo_exts, ext) + logo_heights <- c(logo_heights, height) + logo_widths <- c(logo_widths, width) + logo_sizes <- c(logo_sizes, size) + + } + + } + + logos <- tibble::tibble( + theme = logo_themes, + type = logo_types, + src = logo_urls, + background = logo_backgrounds, + ext = logo_exts, + height = logo_heights, + width = logo_widths, + size = logo_sizes + ) + + # colors ------------------------------------------------------------------ + colors_hex <- content$colors |> purrr::map_chr(purrr::pluck, "hex") + colors_type <- content$colors |> purrr::map_chr(purrr::pluck, "type") + colors_brightness <- content$colors |> purrr::map_int(purrr::pluck, "brightness") + + hex2rgb <- function(hex) { + hold <- grDevices::col2rgb(hex) + r <- hold[1] + g <- hold[2] + b <- hold[3] + out <- glue::glue( + "rgb({r}, {g}, {b})" + ) + out + } + + colors <- tibble::tibble( + hex = colors_hex, + type = colors_type, + brightness = colors_brightness + ) |> + dplyr::mutate( + rgb = purrr::map_chr(hex, hex2rgb) + ) + + # fonts ------------------------------------------------------------------- + fonts_name <- content$fonts |> purrr::map_chr(purrr::pluck, "name") + fonts_type <- content$fonts |> purrr::map_chr(purrr::pluck, "type") + fonts_origin <- content$fonts |> purrr::map_chr(purrr::pluck, "origin") + fonts_origin_id <- content$fonts |> purrr::map_chr(purrr::pluck, "originId") + + fonts <- tibble::tibble( + name = fonts_name, + type = fonts_type, + origin = fonts_origin, + origin_id = fonts_origin_id + ) |> + dplyr::mutate( + gfonts_url = paste0( + "https://fonts.google.com/specimen/", + name ), - tibblify::tib_unspecified("tags"), - tibblify::tib_chr("type") - ), - - tibblify::tib_df( - "colors", - tibblify::tib_chr("hex"), - tibblify::tib_chr("type"), - tibblify::tib_int("brightness"), - ), - - tibblify::tib_df( - "fonts", - tibblify::tib_chr("name"), - tibblify::tib_chr("type"), - tibblify::tib_chr("origin"), - tibblify::tib_chr("originId"), - tibblify::tib_unspecified("weights"), - ), - - tibblify::tib_row( - "company", - tibblify::tib_unspecified("employees"), - tibblify::tib_unspecified("foundedYear"), - tibblify::tib_unspecified("kind"), - tibblify::tib_unspecified("location"), - tibblify::tib_df( - "industries", - tibblify::tib_unspecified("id", required = FALSE), - tibblify::tib_unspecified("parent"), - tibblify::tib_dbl("score", required = FALSE), - tibblify::tib_chr("name", required = FALSE), - tibblify::tib_chr("emoji", required = FALSE), - tibblify::tib_chr("slug", required = FALSE) + import_css = paste0( + "@import url(", + stringr::str_c( + "https://fonts.googleapis.com/css2?family=", + name + ), + ");" + ) + ) + + # company ----------------------------------------------------------------- + company <- content$company |> purrr::compact() + + industries <- company$industries + + industry_names <- industries |> purrr::map_chr(purrr::pluck, "name") + industry_emojis <- industries |> purrr::map_chr(purrr::pluck, "emoji") + industry_slugs <- industries |> purrr::map_chr(purrr::pluck, "slug") + industry_scores <- industries |> purrr::map_dbl(purrr::pluck, "score") + + company_industries <- tibble::tibble( + name = industry_names, + emoji = industry_emojis, + slug = industry_slugs, + score = industry_scores + ) |> + dplyr::arrange( + desc(score) + ) + + + # brand ------------------------------------------------------------------- + + brand <- brand_init |> + dplyr::mutate( + links = list(links), + logos = list(logos), + colors = list(colors), + fonts = list(fonts), + company = list(company_industries) + ) + + return(brand) + +} + +# gmh_brand <- fetch_brand("gmhcommunities.com") +# brand_yml <- yaml::as.yaml(gmh_brand) +# yaml::write_yaml(gmh_brand, "dev/brandfetch/gmh_brand.yml") + +#' Download Brand Logos +#' +#' @param brand Brand +#' @param path Path +#' @param ... ... +#' +#' @return Invisible +#' @export +#' +#' @importFrom dplyr mutate +#' @importFrom fs dir_exists dir_create +#' @importFrom purrr pmap_chr pwalk +download_brand_logos <- function( + brand, + path = "inst/extdata/brand", + ... +) { + + if (!fs::dir_exists(path)) { + fs::dir_create(path) + } + + brand_logos <- brand$logos |> + dplyr::mutate( + file = purrr::pmap_chr( + list( + brand_name = brand$name, + type = type, + format = format, + height = height, + width = width + ), + get_logo_file_name ) ) + + brand_logos |> + purrr::pwalk( + download_logo, + src = brand$logos$src, + name = brand$name, + path = path, + ... + ) + + return( + invisible(TRUE) ) - out <- tibblify::tibblify(content, spec, unspecified = "drop") - out$logos <- out$logos |> tidyr::unnest("formats") - out$company <- out$company |> purrr::pluck("industries") +} - return(out) +#' Get Brand Logos +#' +#' @param brand Brand +#' @param path Path +#' @param ... ... +#' +#' @return Invisible +#' @export +#' +#' @importFrom dplyr mutate +#' @importFrom purrr pmap_chr +#' @importFrom fs dir_exists dir_create +#' @importFrom purrr pwalk pmap_chr +get_brand_logos <- function( + brand, + path, + ... +) { + + brand_logos <- brand$logos |> + dplyr::mutate( + file = purrr::pmap_chr( + list( + brand_name = brand$name, + type = type, + format = format, + height = height, + width = width + ), + get_logo_file_name + ) + ) + + purrr::walk2( + brand_logos$src, + brand_logos$file, + ~download_logo( + src = .x, + file = .y, + name = brand$name, + type = brand_logos$type, + format = brand_logos$format, + height = brand_logos$height, + width = brand_logos$width + ) + ) + + return( + invisible(TRUE) + ) } +#' Download Brand Logo File +#' +#' @description +#' This function downloads a brand logo file from a URL to the specified path. +#' +#' @param src The URL of the logo file +#' @param file The name of the logo file +#' @param name The name of the brand +#' @param path The path to save the logo file +#' @param type The type of logo (icon or logo) +#' @param format The format of the logo (png, svg, jpeg) +#' @param height The height of the logo +#' @param width The width of the logo +#' @param ... Additional arguments +#' +#' @return Invisible +#' @export +#' +#' @importFrom stringr str_replace_all str_to_lower +#' @importFrom fs dir_exists dir_create path +download_logo <- function( + src, + file, + name, + path = "inst/extdata/brand", + type = c("icon", "logo"), + format = c("png", "svg", "jpeg"), + height, + width, + ... +) { + + type <- match.arg(type) + format <- match.arg(format) + height <- as.integer(height) + width <- as.integer(width) + src <- src |> stringr::str_replace_all(" ", "%20") + brand_name_clean <- stringr::str_to_lower(name) |> stringr::str_replace_all(" ", "_") + size <- paste0(as.character(height), "x", as.character(width)) + + if (!fs::dir_exists(path)) { + fs::dir_create(path) + } + + file_path <- fs::path(path, file) + + download.file( + src, + destfile = file_path, + method = "curl" + ) + + return( + invisible(TRUE) + ) + +} -# out_company <- tibble::tibble( -# id = content$id, -# name = content$name, -# domain = domain, -# claimed = content$claimed, -# description = content$description, -# longDescription = content$longDescription, -# links = content$links |> purrr::map_dfr( -# ~ tibble::tibble( -# name = .x$name, -# url = .x$url -# ) -# ), -# qualityScore = content$qualityScore, -# company = content$company |> purrr::map_dfr( -# ~ tibble::tibble( -# industries = .x$industries |> purrr::map_dfr( -# ~ tibble::tibble( -# score = .x$score, -# id = .x$id, -# name = .x$name, -# emoji = .x$emoji, -# parent = .x$parent |> purrr::map_dfr( -# ~ tibble::tibble( -# emoji = .x$emoji, -# id = .x$id, -# name = .x$name, -# slug = .x$slug -# ) -# ), -# slug = .x$slug -# ) -# ), -# kind = .x$kind, -# location = .x$location -# ) -# ) -# -# ) - -# out_logos <- content$logos |> purrr::map_dfr( -# ~ tibble::tibble( -# domain = domain, -# theme = .x$theme, -# src = .x$formats$src, -# background = .x$formats$background, -# format = .x$formats$format, -# height = .x$formats$height, -# width = .x$formats$width, -# size = .x$formats$size, -# tags = .x$tags, -# type = .x$type -# ) -# ) -# -# out_colors <- content$colors |> purrr::map_dfr( -# ~ tibble::tibble( -# domain = domain, -# hex = .x$hex, -# type = .x$type, -# brightness = .x$brightness -# ) -# ) +#' Get Logo File Name +#' +#' @param brand_name Brand Name +#' @param type Type +#' @param theme Theme +#' @param format Format +#' @param height Height +#' @param width Width +#' @param ... ... +#' +#' @return The logo file name +#' @export +#' +#' @importFrom stringr str_replace_all str_to_lower +get_logo_file_name <- function( + brand_name, + type, + theme, + format, + height = NA, + width = NA, + ... +) { + + brand_name_clean <- stringr::str_to_lower(brand_name) |> stringr::str_replace_all(" ", "_") + size <- "" + if (all( + !is.na(height), + !is.na(width), + format != "svg" + )) { + size <- paste0("-", as.character(height), "x", as.character(width)) + } + + paste0( + brand_name_clean, + "-", + type, + "-", + theme, + size, + ".", + format + ) + +} diff --git a/R/generate_palette.R b/R/generate_palette.R new file mode 100644 index 0000000..a60676c --- /dev/null +++ b/R/generate_palette.R @@ -0,0 +1,35 @@ +generate_palette_from_img <- function( + img_path, + num_colors = 40, + ... +) { + + paletter::create_palette( + image_path = img_path, + number_of_colors = num_colors, + ... + ) + +} + +generate_palette_from_color <- function( + color, + modification, + num_colors, + blend_color = NULL, + view_palette = TRUE, + view_labels = TRUE, + ... +) { + + monochromeR::generate_palette( + color = color, + modification = modification, + num_colors = num_colors, + blend_color = blend_color, + view_palette = view_palette, + view_labels = view_labels, + ... + ) + +} diff --git a/R/git_attributes.R b/R/git_attributes.R new file mode 100644 index 0000000..41a3bb4 --- /dev/null +++ b/R/git_attributes.R @@ -0,0 +1,15 @@ +#' Git Attributes +#' +#' @description +#' ... +#' +#' @param ... ... +#' +#' @return ... +#' +#' @export +#' +#' @example examples/ex_git_attributes.R +git_attributes <- function(...) { + +} diff --git a/R/git_config.R b/R/git_config.R new file mode 100644 index 0000000..a949b52 --- /dev/null +++ b/R/git_config.R @@ -0,0 +1,13 @@ +#' Git Config +#' +#' @description +#' ... +#' +#' @param ... ... +#' +#' @return ... +#' +#' @export +#' +#' @example examples/ex_git_config.R +git_config <- function(...) { } \ No newline at end of file diff --git a/R/git_ignore.R b/R/git_ignore.R new file mode 100644 index 0000000..9c6d082 --- /dev/null +++ b/R/git_ignore.R @@ -0,0 +1,13 @@ +#' Git Ignore +#' +#' @description +#' ... +#' +#' @param ... ... +#' +#' @return ... +#' +#' @export +#' +#' @example examples/ex_git_ignore.R +git_ignore <- function(...) { } diff --git a/R/keyring.R b/R/keyring.R index e8b1b51..ce17ef6 100644 --- a/R/keyring.R +++ b/R/keyring.R @@ -21,7 +21,7 @@ init_keyring <- function( msg <- glue::glue( "Keyring '{name}' created successfully.", - "To add secrets to the keyring, use `noclocksR::add_secret()`." + "To add secrets to the keyring, use `noclocksr::add_secret()`." ) rlang::inform(msg) @@ -71,7 +71,7 @@ check_keyring <- function( rlang::abort( glue::glue( "Keyring '{keyring}' does not exist.", - "Please create it using `noclocksR::init_keyring()`." + "Please create it using `noclocksr::init_keyring()`." ) ) } diff --git a/R/noclocks_logo.R b/R/noclocks_logo.R new file mode 100644 index 0000000..7668186 --- /dev/null +++ b/R/noclocks_logo.R @@ -0,0 +1,29 @@ + +# ------------------------------------------------------------------------ +# +# Title : No Clocks Logo +# By : Jimmy Briggs +# Date : 2024-07-26 +# +# ------------------------------------------------------------------------ + + +# internal ---------------------------------------------------------------- + + +# exported ---------------------------------------------------------------- + +# noclocks_logo <- function( +# type = c("symbol", "wordmark"), +# format = c("png", "svg", "jpeg", "webp", "gif"), +# height, +# width, +# ... +# ) { +# +# logos_root_dir <- pkg_sys_assets("logos") +# logo_dir <- fs::path(logos_root_dir, type, format) +# logo_file_name <- glue::glue("noclocks_logo_{type}.{format}") +# +# +# } diff --git a/R/onLoad.R b/R/onLoad.R index 8975dcd..0bc9455 100644 --- a/R/onLoad.R +++ b/R/onLoad.R @@ -4,13 +4,13 @@ #' These functions are run when the package is loaded or attached. .onAttach <- function( - libname = find.package("noclocksR"), - pkgname = "noclocksR" + libname = find.package("noclocksr"), + pkgname = "noclocksr" ) { vers <- as.character(utils::packageVersion(pkgname)) msg <- sprintf( - "Welcome to `noclocksR`! This is version: %s\n", + "Welcome to `noclocksr`! This is version: %s\n", vers ) @@ -38,7 +38,7 @@ # # .auth_env <<- rlang::env( # auth_state = gargle::init_AuthState( - # package = "noclocksR", + # package = "noclocksr", # client = oauth_client, # auth_active = TRUE # ) diff --git a/R/pdf.R b/R/pdf.R new file mode 100644 index 0000000..9857b79 --- /dev/null +++ b/R/pdf.R @@ -0,0 +1,267 @@ +#' Process PDF Invoices and Receipts +#' +#' @description +#' This function processes PDF invoices and receipts by extracting the content, +#' parsing the content, and saving the files to the output directory. +#' +#' @details +#' This function implements PDF extraction by extracting the PDF content via +#' [pdftools::pdf_text()] and parsing the extracted text into the following +#' components: +#' - Document Type (Receipt or Invoice) +#' - Date +#' - Company Name +#' - ID (Receipt or Invoice Number) +#' - New File Name (Formatted as `YYYY-MM-DD-Company-DocumentType-ID.pdf`) +#' +#' The PDF file is then copied and renamed using the new file name inside of the +#' specified output directory and archived in the specified archive directory. +#' +#' Logs of the processing are written to a log file. +#' +#' @param input_dir (Required) The directory containing the PDF files to process. +#' @param output_dir (Required) The directory to save the processed PDF files. +#' @param archive_dir (Optional) The directory to archive the processed PDF files. +#' @param log_file (Optional) The path to the log file. Default is `getOption("log_file")`, +#' and if that is not set it will default to the path `Logs/` in the specified +#' output directory. +#' @param ... Additional arguments +#' +#' @return A list of the processed PDF files in the output directory. +#' @export +#' +#' @importFrom fs dir_exists dir_info dir_ls path_dir path file_exists file_create +#' @importFrom cli cli_progress_bar cli_progress_update cli_progress_done +#' @importFrom assertthat assert_that +#' @importFrom stringr str_extract str_trim +#' @importFrom pdftools pdf_text pdf_data +#' @importFrom purrr map pluck +#' @importFrom glue glue +#' +#' @examples +#' \dontrun{ +#' +#' fs::dir_copy(fs::path_package("noclocksr", "PDFs", "Input"), fs::path("Input")) +#' +#' process_pdfs( +#' input_dir = , +#' output_dir = fs::path("Output"), +#' archive_dir = fs::path("Archive"), +#' log_file = fs::path("Logs", paste0(Sys.Date(), ".log")) +#' ) +#' } +#' +#' @seealso +#' [extract_pdf_content()], [parse_pdf_content()] +#' [pdftools::pdf_text()], [pdftools::pdf_data()] +process_pdfs <- function( + input_dir, + output_dir, + archive_dir = fs::path(input_dir, "archive", Sys.Date()), + log_file = getOption("log_file", fs::path(output_dir, "Logs", paste0(Sys.Date(), ".log"))), + ... +) { + if (!is.null(log_file)) { + if (!fs::file_exists(log_file)) { + if (!fs::dir_exists(fs::path_dir(log_file))) { + fs::dir_create(fs::path_dir(log_file), recurse = TRUE) + } + fs::file_create(log_file) + } + if (is.null(getOption("log_file"))) { + options("log_file" = log_file) + } + } else { + cli::cli_warn("⚠ī¸ No log file specified. Logging to console only.") + } + + write_log("Setting up PDF Processing...", log_lvl = "INFO", event = "Start") + + invisible(assertthat::assert_that(fs::dir_exists(input_dir), nrow(fs::dir_info(input_dir, glob = "*.pdf")) > 0, msg = "The input directory must exist and contain PDF files.")) + + if (!fs::dir_exists(output_dir)) { + fs::dir_create(output_dir, recurse = TRUE) + write_log(glue::glue("Created output directory: {output_dir}"), log_lvl = "INFO", event = "Success") + } + + pdf_files <- fs::dir_ls(input_dir, glob = "*.pdf") + + write_log(glue::glue("Found {length(pdf_files)} PDF files in {input_dir}"), log_lvl = "INFO", event = "Search") + + cli::cli_progress_bar(name = "Processing PDFs", total = length(pdf_files)) + + for (pdf_file in pdf_files) { + write_log(glue::glue("Processing {pdf_file}..."), log_lvl = "INFO", event = "Process") + + pdf_content <- extract_pdf_content(pdf_file) + pdf_content_parsed <- tryCatch({ + parse_pdf_content(pdf_content) + }, error = function(e) { + write_log(glue::glue("Error processing {pdf_file}: {e$message}"), log_lvl = "ERROR", event = "Error") + next + }) + + output_file <- fs::path(output_dir, paste0(pdf_content_parsed$type, "s"), pdf_content_parsed$company, pdf_content_parsed$new_file_name) + + if (fs::file_exists(output_file)) { + write_log(glue::glue("File {output_file} already exists. Overwriting..."), log_lvl = "WARNING", event = "Warning") + } + + fs::dir_create(dirname(output_file), recurse = TRUE) + fs::file_copy(pdf_file, output_file, overwrite = TRUE) + + write_log(glue::glue("Processed {pdf_file} to {output_file}"), log_lvl = "INFO", event = "Success") + + if (!fs::dir_exists(archive_dir)) { + fs::dir_create(archive_dir, recurse = TRUE) + } + + fs::file_move(pdf_file, fs::path(archive_dir, basename(pdf_file))) + + write_log(glue::glue("Archived {pdf_file} to {archive_dir}"), log_lvl = "INFO", event = "Saved") + + cli::cli_progress_update() + + write_log(glue::glue("Finished processing {pdf_file}"), log_lvl = "INFO", event = "Stop") + } + + cli::cli_progress_done() + + invisible(fs::dir_info(output_dir, glob = "*.pdf")) +} + +#' Extract PDF Content +#' +#' @description +#' Extracts the text content from a PDF file. +#' +#' @param path (Required) The path to the PDF file. +#' @param ... Additional arguments +#' +#' @return A character vector containing the text content of the PDF. +#' @export +#' +#' @importFrom assertthat assert_that +#' @importFrom fs path_ext file_exists +#' @importFrom pdftools pdf_text +extract_pdf_content <- function(path, ...) { + assertthat::assert_that(fs::path_ext(path) == "pdf", msg = "The file must be a PDF.") + assertthat::assert_that(fs::file_exists(path), msg = "The file does not exist.") + + pdf_content <- pdftools::pdf_text(path) + + assertthat::assert_that(!is.null(pdf_content), class(pdf_content) == "character", length(pdf_content) > 0, stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] %in% c("Receipt", "Invoice"), msg = "The extracted PDF content is not valid.") + + return(pdf_content) +} + +#' Parse PDF Content +#' +#' @description +#' Parses the extracted text content from a PDF file. +#' +#' @param pdf_content (Required) The text content extracted from the PDF file. +#' @param ... Additional arguments +#' +#' @return A list containing parsed details: type, date, company, id, new_file_name. +#' @export +#' +#' @importFrom assertthat assert_that +#' @importFrom stringr str_extract str_trim boundary +#' @importFrom glue glue +parse_pdf_content <- function(pdf_content, ...) { + assertthat::assert_that(!is.null(pdf_content), class(pdf_content) == "character", length(pdf_content) > 0, stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] %in% c("Receipt", "Invoice"), msg = "The extracted PDF content is not valid.") + + doc_type <- stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] + assertthat::assert_that(!is.null(doc_type), msg = "The document type could not be determined.") + + date_raw <- stringr::str_extract(pdf_content, "(?<=Date paid |paid on |Date of issue |due )[A-Za-z]+ \\d{1,2}, \\d{4}") + assertthat::assert_that(!is.na(date_raw), !is.null(date_raw), msg = "The date could not be extracted.") + + date <- format(as.Date(date_raw, format = "%B %d, %Y"), "%Y-%m-%d") |> as.character() + assertthat::assert_that(!is.na(date), !is.null(date), nchar(date) == 10, msg = "The date could not be formatted.") + + company_line <- stringr::str_extract(pdf_content, "(?<=\\n)[A-Za-z\\. ]+(?=\\n\\d{4} |\\n\\d{3,5} )") + if (is.na(company_line)) { + company_line <- stringr::str_extract(pdf_content, "(?<=\\n)[A-Za-z\\.,\\s]+(?=\\s+Bill to)") + } + company_line <- company_line |> stringr::str_trim() + company <- stringr::str_extract(company_line, "[A-Za-z\\.]+") + assertthat::assert_that(!is.na(company), !is.null(company), company != "", msg = "Company name not found or invalid") + + id <- ifelse(doc_type == "Receipt", stringr::str_extract(pdf_content, "(?<=Receipt number )[\\d\\-]+"), stringr::str_extract(pdf_content, "(?<=Invoice number )[A-Z\\d\\-]+")) + assertthat::assert_that(!is.na(id), id != "", msg = "ID not found or invalid") + + new_file_name <- glue::glue("{date}-{company}-{doc_type}-{id}.pdf") + + list(type = doc_type, date = date, company = company, id = id, new_file_name = new_file_name) +} + +#' Write Log +#' +#' @description +#' Write log messages to the console and a log file. +#' +#' @param message (Required) Character string of the message to log. +#' @param log_file (Optional) The path to the log file. Default is `getOption("log_file")`. +#' @param log_lvl (Optional) The log level. Default is `INFO`. +#' @param event (Optional) The event type. Default is `Process`. +#' +#' @return Invisibly returns the log message. +#' @export +#' +#' @importFrom fs file_exists dir_exists dir_create file_create +#' @importFrom cli cat_line cli_warn +#' @importFrom glue glue +write_log <- function(message, log_file = getOption("log_file"), log_lvl = "INFO", event = "Process") { + if (!is.null(log_file)) { + if (!fs::file_exists(log_file)) { + if (!fs::dir_exists(fs::path_dir(log_file))) { + fs::dir_create(fs::path_dir(log_file), recurse = TRUE) + } + fs::file_create(log_file) + } + } else { + cli::cli_warn("⚠ī¸ No log file specified. Logging to console only.") + } + + color <- switch( + log_lvl, + TRACE = "gray", + DEBUG = "orange", + INFO = "cyan", + WARNING = "yellow", + WARN = "yellow", + ERROR = "red", + FATAL = "maroon", + CRITICAL = "darkred", + "black" + ) + + event_sym <- switch( + event, + Start = "🚀", + Stop = "🛑", + Pause = "⏸", + Search = "🔍", + Process = "🔄", + Success = "✅", + Failure = "❌", + Saved = "💾", + Loaded = "📂", + Info = "ℹī¸", + Warning = "⚠ī¸", + Error = "❌", + Fatal = "💀", + Critical = "🚨", + "📝" + ) + + cli::cat_line(glue::glue("{event_sym} [{log_lvl}]: {message}"), col = color) + + msg <- glue::glue("\n{Sys.time()} [{log_lvl}]: {message}\n") + + if (!is.null(log_file)) { + cat(msg, file = log_file, append = TRUE, sep = "\n") + } +} diff --git a/R/pkg_changelog.R b/R/pkg_changelog.R new file mode 100644 index 0000000..850b8e1 --- /dev/null +++ b/R/pkg_changelog.R @@ -0,0 +1,74 @@ + +# ------------------------------------------------------------------------ +# +# Title : Package CHANGELOG.md +# By : Jimmy Briggs +# Date : 2024-09-14 +# +# ------------------------------------------------------------------------ + +# internal ---------------------------------------------------------------- + +.git_cliff_config_url <- "https://raw.githubusercontent.com/noclocks/.github/main/workflow-templates/cliff.template.toml" + +.git_cliff_changelog_gha_url <- "https://raw.githubusercontent.com/noclocks/.github/main/.github/workflows/changelog.yml" + +git_cliff <- function( + changelog_path = "CHANGELOG.md", + config_path = ".github/cliff.toml", + open = rlang::is_interactive() +) { + + cmd <- "git-cliff.exe" + + if (!test_sys_path(cmd)) { + rlang::abort( + c( + "{.code {cmd}} not found on the system's {.code PATH}." + ) + ) + } + + full_cmd <- paste0( + cmd, " -o ", changelog_path, " -c ", config_path + ) + + shell(full_cmd) + + cli::cli_alert_success("Git Cliff has successfully generated the changelog.") + if (open) { file.edit(changelog_path) } + + return(invisible(0)) + +} + +use_git_cliff <- function( + path = "CHANGELOG.md", + config = ".github/cliff.toml" +) { + + # get the cliff config file + if (!file.exists(config)) { + download.file(url = .git_cliff_config_url, destfile = config) + } + + # get the cliff changelog action file + if (!file.exists(".github/workflows/changelog.yml")) { + download.file(url = .git_cliff_changelog_gha_url, destfile = ".github/workflows/changelog.yml") + } + + # load the yaml file + yaml <- yaml::yaml.load_file(".github/workflows/changelog.yml") + + # ensure the output in yaml points to the changelog path + yaml$jobs$changelog$steps[[2]]$with$output <- path + + # ensure the template-config in yaml points to the config path + yaml$jobs$changelog$steps[[2]]$with$`template-config` <- config + + # write the yaml back to the file + yaml::write_yaml(yaml, ".github/workflows/changelog.yml") + + cli::cli_alert_success("Git Cliff has been successfully configured.") + +} diff --git a/R/pkg_news.R b/R/pkg_news.R new file mode 100644 index 0000000..f29c17e --- /dev/null +++ b/R/pkg_news.R @@ -0,0 +1,573 @@ +# ------------------------------------------------------------------------ +# +# Title : Package NEWS.md Management +# By : Jimmy Briggs +# Date : 2024-09-14 +# +# ------------------------------------------------------------------------ + +# generate_news ----------------------------------------------------------- + +#' Generate `NEWS.md` +#' +#' @description +#' These functions generate the R package's `NEWS.md` file. +#' +#' @details +#' - `generate_news()`: Generates a `NEWS.md` file by calling +#' `generate_news_from_changelog()` with default settings, +#' and will default to a generic `NEWS.md` file if no `CHANGELOG.md` +#' file is found. +#' +#' - `generate_news_from_changelog()`: Generates a `NEWS.md` file from a +#' pre-existing `CHANGELOG.md` file. It parses the Markdown content of +#' the `CHANGELOG.md` file, extracts version headers, sections, and their +#' content, and organizes them into a structured format suitable for +#' a typical R package's `NEWS.md` file. +#' +#' @param input_file Path to the `CHANGELOG.md` file. +#' @param output_file Path to the output `NEWS.md` file. +#' @param include_unreleased Logical indicating whether to include the +#' `[Unreleased]` section (default: `TRUE`). +#' @param remove_commits Logical indicating whether to remove commit hashes +#' and authors from the list items (default: `TRUE`). +#' @param version_pattern Regular expression pattern to match version headers +#' (default: `^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]`). +#' @param ordered_groups Character vector specifying the ordered groups for +#' sections in the `NEWS.md` file (default: `.ordered_groups`). +#' The default order is based on the significance of the groups. +#' @param skip_groups Character vector specifying the groups to skip when +#' generating the `NEWS.md` file (default: `NULL`). +#' @param section_name_mapping Named character vector to map section names +#' to custom names in the `NEWS.md` file (default: `NULL`). +#' The names should match the group names in the `CHANGELOG.md` file. +#' @param verbose Logical indicating whether to display messages (default: `TRUE`). +#' @param overwrite Logical indicating whether to overwrite existing `NEWS.md` file (default: `FALSE`). +#' @param pkg_name Package name (default: `NULL`). If `NULL`, reads from `DESCRIPTION`. +#' @param pkg_version Package version (default: `NULL`). If `NULL`, reads from `DESCRIPTION`. +#' @param pkg_path Path to the package directory containing `DESCRIPTION` (default: `NULL`). +#' @param ... Arguments passed on to `generate_news_from_changelog()` from +#' `generate_news()`. +#' +#' @return Both functions invisibly return the generated `news_content` +#' as a character vector. +#' +#' @export +#' +#' @seealso [use_github_action_news()] for implementing this into a GitHub Action +#' Workflow. +#' +#' @importFrom markdown markdownToHTML +#' @importFrom xml2 read_html xml_children xml_find_first xml_name xml_text xml_find_all +#' @importFrom stringr str_detect str_match str_replace str_trim +#' @importFrom rlang abort +#' @importFrom cli cli_alert_success cli_alert_info cli_alert_warning +#' @importFrom usethis use_news_md +#' +#' @examples +#' if (interactive()) { +#' +#' # Examples of using the `generate_news()` function: +#' generate_news() +#' +#' # Examples of using the `generate_news_from_changelog()` function: +#' +#' # Generate NEWS.md from CHANGELOG.md using all default settings +#' generate_news_from_changelog() +#' +#' # Specify custom input and output files +#' generate_news_from_changelog( +#' input_file = "path/to/your/CHANGELOG.md", +#' output_file = "path/to/your/NEWS.md" +#' ) +#' +#' # Overwrite the existing NEWS.md file +#' generate_news_from_changelog(overwrite = TRUE) +#' +#' # Exclude the 'Unreleased' section and keep commit hashes in the list items +#' generate_news_from_changelog(include_unreleased = FALSE, remove_commits = FALSE) +#' +#' # Skip certain sections +#' generate_news_from_changelog(skip_groups = c("Miscellaneous Tasks", "Meta")) +#' +#' # Map section names to custom names +#' generate_news_from_changelog( +#' section_name_mapping = c("Added" = "Features", "Fixed" = "Bug Fixes") +#' ) +#' +#' # Use custom ordered groups +#' custom_ordered_groups <- c( +#' "Breaking Changes", "Features", "Bug Fixes", "Documentation", "Testing" +#' ) +#' generate_news_from_changelog(ordered_groups = custom_ordered_groups) +#' +#' } +generate_news <- function( + output_file = "NEWS.md", + ... +) { + + if (!file.exists("CHANGELOG.md") && !file.exists("inst/CHANGELOG.md")) { + + cli::cli_alert_info( + c( + "No {.code CHANGELOG.md} file found in the package directory.", + "Using default {.code NEWS.md}." + ) + ) + + usethis::use_news_md(open = FALSE) + + res <- readLines("NEWS.md") + + return(invisible(res)) + + } + + clog_path <- if (file.exists("CHANGELOG.md")) { + "CHANGELOG.md" + } else { + "inst/CHANGELOG.md" + } + + cli::cli_alert_info( + c( + "Generating {.code NEWS.md} from {.code CHANGELOG.md}." + ) + ) + + res <- generate_news_from_changelog( + input_file = clog_path, + output_file = output_file, + ... + ) + + return(invisible(res)) + +} + +# generate_news_from_changelog -------------------------------------------- + +#' @rdname generate_news +#' +#' @export +generate_news_from_changelog <- function( + input_file = "CHANGELOG.md", + output_file = "NEWS.md", + include_unreleased = TRUE, + remove_commits = TRUE, + version_pattern = "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]", + ordered_groups = .ordered_groups, + skip_groups = NULL, + section_name_mapping = NULL, + verbose = TRUE, + overwrite = FALSE, + pkg_name = NULL, + pkg_version = NULL, + pkg_path = NULL +) { + + # Check if input file exists and read content + if (!file.exists(input_file)) { + rlang::abort("Input file does not exist: {.path {input_file}}.") + } + if (verbose) { + cli::cli_alert_info("Reading {.path {input_file}}") + } + + changelog_content <- readLines(input_file) + if (length(changelog_content) == 0) { + rlang::abort("The {.code CHANGELOG.md} file is empty.") + } + + # Convert Markdown to HTML + if (verbose) { + cli::cli_alert_info("Converting Markdown to HTML") + } + changelog_html <- markdown::markdownToHTML( + text = changelog_content, + fragment.only = TRUE + ) + if (length(changelog_html) == 0) { + rlang::abort("Failed to convert CHANGELOG.md to HTML. The resulting HTML is empty.") + } + + + # Parse HTML content + if (verbose) { + cli::cli_alert_info("Parsing HTML content") + } + changelog_xml <- xml2::read_html(changelog_html) + + # Get all nodes under the body + body_nodes <- xml2::xml_children( + xml2::xml_find_first( + changelog_xml, + ".//body" + ) + ) + + # Initialize variables to store versions and their sections + versions <- list() + current_version <- NULL + current_section <- NULL + + heading_levels <- c("h2", "h3", "h4", "h5", "h6") + + # Loop through the body nodes to collect content under each version and section + for (node in body_nodes) { + node_name <- xml2::xml_name(node) + node_text <- xml2::xml_text(node) + + if (stringr::str_detect(node_text, version_pattern)) { + # New version header found + current_version <- node_text + versions[[current_version]] <- list() + current_section <- NULL + if (verbose) { + cli::cli_alert_info("Found version: {current_version}") + } + } else if (node_name %in% heading_levels && !is.null(current_version)) { + # New section under the current version + current_section <- node_text + versions[[current_version]][[current_section]] <- list() + if (verbose) { + cli::cli_alert_info(" Found section: {current_section}") + } + } else { + # Add node to the current section of the current version + if (!is.null(current_version) && !is.null(current_section)) { + versions[[current_version]][[current_section]] <- c( + versions[[current_version]][[current_section]], list(node) + ) + } + } + } + + # Read package name and version from DESCRIPTION or use provided values + if (is.null(pkg_name) || is.null(pkg_version)) { + if (is.null(pkg_path)) { + pkg_path <- "." + } + description_file <- file.path(pkg_path, "DESCRIPTION") + if (!file.exists(description_file)) { + rlang::abort( + c( + "{.code DESCRIPTION} file not found at {.path {description_file}}.", + "Please provide the {.arg pkg_name} and {.arg pkg_version} arguments, or specify the {.arg pkg_path}." + ) + ) + } + if (verbose) { + cli::cli_alert_info("Reading package info from {.path {description_file}}") + } + description_content <- read.dcf(description_file) + if (is.null(pkg_name)) { + pkg_name <- description_content[1, "Package"] + if (verbose) { + cli::cli_alert_info("Using package name from DESCRIPTION: {pkg_name}") + } + } + if (is.null(pkg_version)) { + pkg_version <- description_content[1, "Version"] + if (verbose) { + cli::cli_alert_info("Using package version from DESCRIPTION: {pkg_version}") + } + } + } else { + if (verbose) { + cli::cli_alert_info("Using provided package name: {pkg_name}") + cli::cli_alert_info("Using provided package version: {pkg_version}") + } + } + + # Initialize NEWS.md content + news_content <- character() + + # Process versions in order, placing '[Unreleased]' first if included + version_names <- names(versions) + + # Extract version numbers + version_numbers <- sapply(version_names, function(vn) { + vm <- stringr::str_match( + vn, + "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]\\s*-?\\s*(.*)$" + ) + vm[1, 2] + }) + + # Identify 'Unreleased' and other versions + is_unreleased <- version_numbers == "Unreleased" + unreleased_versions <- version_names[is_unreleased] + other_versions <- version_names[!is_unreleased] + + # Convert other version numbers to package_version for sorting + other_version_numbers <- version_numbers[!is_unreleased] + parsed_versions <- package_version(other_version_numbers) + + # Order other versions in decreasing order + order_indices <- order(parsed_versions, decreasing = TRUE) + other_versions <- other_versions[order_indices] + + # Combine versions based on include_unreleased flag + if (include_unreleased && length(unreleased_versions) > 0) { + version_names_ordered <- c(unreleased_versions, other_versions) + } else { + version_names_ordered <- other_versions + } + + # Now process versions in order + for (version_name in version_names_ordered) { + # Extract version number and date if available + version_header <- version_name + version_match <- stringr::str_match( + version_name, + "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]\\s*-?\\s*(.*)$" + ) + version_number <- version_match[1, 2] + version_date <- version_match[1, 3] + + # Skip 'Unreleased' if not included + if (!include_unreleased && version_number == "Unreleased") next + + # Build version header + if (!is.na(version_number)) { + version_header <- sprintf("# %s %s", pkg_name, version_number) + if (version_date != "") { + version_header <- sprintf("%s (%s)", version_header, version_date) + } + } else { + # If version header doesn't match expected pattern, use it as is + version_header <- sprintf("# %s %s", pkg_name, version_name) + } + + # Add version header to NEWS.md + news_content <- c(news_content, version_header, "") + + # Get the sections under the current version + sections <- versions[[version_name]] + + # Process sections in the order of significance + for (group_name in ordered_groups) { + # Skip groups if specified + if (!is.null(skip_groups) && group_name %in% skip_groups) next + + if (group_name %in% names(sections)) { + # Map section name if mapping is provided + mapped_name <- if (!is.null(section_name_mapping) && group_name %in% names(section_name_mapping)) { + section_name_mapping[[group_name]] + } else { + group_name + } + + # Add the section heading with appropriate heading level + news_section_header <- sprintf("## %s", mapped_name) + news_content <- c(news_content, news_section_header, "") + + # Process the nodes in sections[[group_name]] + for (node in sections[[group_name]]) { + node_name <- xml2::xml_name(node) + if (node_name %in% c("ul", "ol")) { + # List items + items <- xml2::xml_find_all(node, ".//li") + for (item in items) { + item_text <- xml2::xml_text(item) + # Optionally clean up item_text to remove commit hashes and authors + if (remove_commits) { + item_text <- stringr::str_replace( + item_text, + "\\s*\\(\\w{7}\\)\\s*-\\s*\\(.*?\\)\\s*$", + "" + ) + } + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } else { + # Other text, add as is if not empty + item_text <- stringr::str_trim(xml2::xml_text(node)) + if (item_text != "") { + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } + } + + # Add an empty line after each section + news_content <- c(news_content, "") + } + } + + # Process any remaining sections not in ordered_groups + remaining_sections <- setdiff(names(sections), ordered_groups) + for (section_name in remaining_sections) { + # Skip groups if specified + if (!is.null(skip_groups) && section_name %in% skip_groups) next + + # Map section name if mapping is provided + mapped_name <- if (!is.null(section_name_mapping) && section_name %in% names(section_name_mapping)) { + section_name_mapping[[section_name]] + } else { + section_name + } + + # Add the section heading + news_section_header <- sprintf("## %s", mapped_name) + news_content <- c(news_content, news_section_header, "") + + # Process the nodes in sections[[section_name]] + for (node in sections[[section_name]]) { + node_name <- xml2::xml_name(node) + if (node_name %in% c("ul", "ol")) { + items <- xml2::xml_find_all(node, ".//li") + for (item in items) { + item_text <- xml2::xml_text(item) + # Optionally clean up item_text to remove commit hashes and authors + if (remove_commits) { + item_text <- stringr::str_replace( + item_text, + "\\s*\\(\\w{7}\\)\\s*-\\s*\\(.*?\\)\\s*$", + "" + ) + } + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } else { + item_text <- stringr::str_trim(xml2::xml_text(node)) + if (item_text != "") { + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } + } + + # Add an empty line after each section + news_content <- c(news_content, "") + } + } + + # Print the news content if verbose + if (verbose) { + cli::cli_alert_info("Generated NEWS.md content:") + cat(news_content, sep = "\n") + } + + # Write the NEWS.md content to the output file + if (file.exists(output_file) && !overwrite) { + rlang::abort( + c( + "Output file already exists: {.path {output_file}}.", + "Use `overwrite = TRUE` to overwrite." + ) + ) + } + + writeLines(news_content, output_file) + if (verbose) { + cli::cli_alert_success("{.path {output_file}} file generated successfully.") + } + + return(invisible(news_content)) +} + + +# use_github_action_news ---------------------------------------- + +#' Generate GitHub Action Workflow for NEWS.md Generation +#' +#' @description +#' This function generates a GitHub Action workflow YAML file that automates +#' the generation of `NEWS.md` from `CHANGELOG.md` whenever changes are pushed +#' to the repository. +#' +#' @param file_name Name of the output workflow file (default: `news.yml`). +#' @param changelog_path Path to the `CHANGELOG.md` file (default: `CHANGELOG.md`). +#' @param config_path Path to the `cliff.toml` configuration file (default: `.github/cliff.toml`). +#' @param overwrite Logical indicating whether to overwrite the existing workflow file (default: `TRUE`). +#' @param verbose Logical indicating whether to display messages (default: `TRUE`). +#' +#' @return Invisibly returns `NULL`. +#' +#' @export +#' +#' @importFrom rlang abort +#' @importFrom cli cli_alert_success cli_alert_info +#' +#' @examples +#' if (interactive()) { +#' generate_github_action_workflow() +#' } +use_github_action_news <- function( + file_name = "news.yml", + news_md_path = "NEWS.md", + changelog_path = "CHANGELOG.md", + overwrite = TRUE, + verbose = TRUE +) { + + output_file <- file.path(".github", "workflows", file_name) + + if (file.exists(output_file) && !overwrite) { + rlang::abort( + c( + "Output file already exists: {.path {output_file}}.", + "Use `overwrite = TRUE` to overwrite." + ) + ) + } + + if (verbose) { + cli::cli_alert_info("Generating GitHub Action workflow at {.path {output_file}}") + } + + workflow_template <- "github-workflows/news.yml.template" + + workflow_template_params <- list( + changelog_path = changelog_path, + news_md_path = news_md_path, + token = "${{ secrets.GITHUB_TOKEN }}" + ) + + usethis::use_template( + workflow_template, + output_file, + data = workflow_template_params, + package = "noclocksr" + ) + + if (verbose) { + cli::cli_alert_success("GitHub Action workflow file created at {.path {output_file}}") + } + + return(invisible(NULL)) +} + +# Internal ---------------------------------------------------------------- + +.ordered_groups <- c( + "Features", + "Added", + "Bug Fixes", + "Fixed", + "Changed", + "Performance", + "Security", + "Refactoring", + "Testing", + "Documentation", + "Configuration", + "Design", + "Cleanup", + "Infrastructure", + "DevOps", + "Deployment", + "Application", + "API", + "Data", + "Database", + "Setup", + "Styling", + "Miscellaneous Tasks", + "Meta" +) diff --git a/R/shiny_assets.R b/R/shiny_assets.R new file mode 100644 index 0000000..950ab71 --- /dev/null +++ b/R/shiny_assets.R @@ -0,0 +1,55 @@ + +# ------------------------------------------------------------------------ +# +# Title : No Clocks R Shiny Assets +# By : Jimmy Briggs +# Date : 2024-07-26 +# +# ------------------------------------------------------------------------ + + +# internal ---------------------------------------------------------------- + +# https://www.jsdelivr.com/package/npm/js-cookie +.dep_jscookie <- htmltools::htmlDependency( + name = "js-cookie", + version = "3.0.5", + src = "https://cdn.jsdelivr.net/npm/js-cookie/dist/", + meta = NULL, + script = "js.cookie.min.js", + stylesheet = NULL, + head = NULL, + attachment = NULL, + package = NULL +) + +# noclocks dependencies --------------------------------------------------- + +noclocks_shiny_dependency <- function() { + htmltools::htmlDependency( + name = "noclocksShiny", + version = utils::packageVersion("noclocksr"), + src = c(href = "noclocksr", file = "assets"), + package = "noclocksr", + script = "noclocksShiny.script.min.js", + stylesheet = "noclocksShiny.styles.min.css", + all_files = FALSE + ) +} + +attach_noclocks_shiny_dependency <- function( + tag, + widget = NULL, + extra_deps = NULL +) { + + + deps <- noclocks_shiny_dependency() + + + + htmltools::attachDependencies( + htmltools::tagList(), + noclocks_shiny_dependency() + ) +} diff --git a/R/shiny_layouts.R b/R/shiny_layouts.R new file mode 100644 index 0000000..b773e47 --- /dev/null +++ b/R/shiny_layouts.R @@ -0,0 +1,37 @@ +# +# # ------------------------------------------------------------------------ +# # +# # Title : Shiny Layouts +# # By : Jimmy Briggs +# # Date : 2024-07-26 +# # +# # ------------------------------------------------------------------------ +# +# +# # internal ---------------------------------------------------------------- +# +# +# +# # dashboard page ---------------------------------------------------------- +# +# dash_page <- function( +# ..., +# dark = TRUE, +# title = NULL, +# theme = noclocks_shiny_theme(), +# favicon = noclocks_favicon() +# ) { +# +# head_tag <- htmltools::tags$head( +# +# ) +# +# } +# +# title, sidebar, body) { +# dashboardPage( +# dashboardHeader(title = title), +# dashboardSidebar(sidebar), +# dashboardBody(body) +# ) +# } diff --git a/R/shiny_resume.R b/R/shiny_resume.R index 59619b6..9019a23 100644 --- a/R/shiny_resume.R +++ b/R/shiny_resume.R @@ -6,12 +6,12 @@ shiny_resume_deps <- function() { "5.0.6", src = system.file( "shiny/startbootstrap-resume-gh-pages/", - package = "noclocksR" + package = "noclocksr" ), stylesheet = list.files( system.file( "shiny/startbootstrap-resume-gh-pages/", - package = "noclocksR" + package = "noclocksr" ), pattern = "\\.css$", recursive = TRUE @@ -19,7 +19,7 @@ shiny_resume_deps <- function() { script = list.files( system.file( "shiny/startbootstrap-resume-gh-pages/", - package = "noclocksR" + package = "noclocksr" ), pattern = "\\.js$", recursive = TRUE diff --git a/R/slack.R b/R/slack.R index e69de29..099d869 100644 --- a/R/slack.R +++ b/R/slack.R @@ -0,0 +1,32 @@ +setup_slack <- function( + slack_config = config::get("slack"), + slack_config_file = here::here("slack_config.dcf"), + ... +) { + + if (is.null(slack_config)) { + rlang::abort("No Slack configuration found") + } + + req_cfg <- c("oauth_access_token", "incoming_webhook_url", "channel") + + stopifnot( + all(req_cfg %in% names(slack_config)) + ) + + slackr::slackr_setup( + channel = slack_config$channel, + username = "slackr", + icon_emoji = ":robot_face:", + incoming_webhook_url = slack_config$incoming_webhook_url, + token = slack_config$oauth_access_token#, + # config_file = "inst/config/slackr.dcf", + # echo = FALSE, + # cache_dir = "data-raw/cache/slackr" + ) + +} + +get_slack_channels <- function() { + slackr::slackr_channels() +} diff --git a/R/sysdata.rda b/R/sysdata.rda new file mode 100644 index 0000000..221fa2f Binary files /dev/null and b/R/sysdata.rda differ diff --git a/R/utils_branding.R b/R/utils_branding.R deleted file mode 100644 index 222df6f..0000000 --- a/R/utils_branding.R +++ /dev/null @@ -1,319 +0,0 @@ - -# ------------------------------------------------------------------------ -# -# Title : Branding Utilities -# By : Jimmy Briggs -# Date : 2024-06-17 -# -# ------------------------------------------------------------------------ - - -#' Fetch a Brand using the Brandfetch API -#' -#' @description -#' This function fetches a brand using the -#' [Brandfetch Brand API](https://docs.brandfetch.com/reference/brand-api). -#' -#' @param domain The domain of the brand to fetch -#' @param brandfetch_api_key The API key for the Brandfetch API -#' @param ... Additional arguments -#' -#' @return A tibble with the brand information -#' -#' @export -#' -#' @example examples/ex_brandfetch.R -#' -#' @importFrom httr2 request req_url_path_append req_method req_auth_bearer_token req_headers req_perform -#' @importFrom tibblify tibblify tspec_object tib_chr tib_lgl tib_dbl tib_unspecified tib_df tib_row -#' @importFrom purrr pluck -#' @importFrom tidyr unnest -#' @importFrom rlang abort -#' @importFrom config get -#' @importFrom tibble tibble -#' @importFrom httr2 resp_body_json -#' @importFrom dplyr pull -fetch_brand <- function( - domain, - brandfetch_api_key = Sys.getenv("BRANDFETCH_API_KEY", unset = config::get("brandfetch_api_key")), - ... -) { - - base_url <- "https://api.brandfetch.io/v2/brands" - - if (is.null(brandfetch_api_key)) { - brandfetch_api_key <- config::get("brandfetch_api_key") - } - - if (is.null(brandfetch_api_key)) { - rlang::abort("No Brandfetch API key found") - } - - req <- httr2::request( - base_url = base_url - ) |> - httr2::req_url_path_append( - domain - ) |> - httr2::req_method("GET") |> - httr2::req_auth_bearer_token(brandfetch_api_key) |> - httr2::req_headers( - `Accept` = "application/json", - `Content-Type` = "application/json" - ) - - res <- req |> httr2::req_perform() - if (res$status_code != 200) { - rlang::abort("Brandfetch API request failed") - } - - content <- res |> - httr2::resp_body_json() - - spec <- tibblify::tspec_object( - tibblify::tib_chr("id", required = FALSE), - tibblify::tib_chr("name", required = FALSE), - tibblify::tib_chr("domain", required = FALSE), - tibblify::tib_lgl("claimed", required = FALSE), - tibblify::tib_chr("description", required = FALSE), - tibblify::tib_chr("longDescription", required = FALSE), - tibblify::tib_dbl("qualityScore", required = FALSE), - tibblify::tib_unspecified("images"), - - tibblify::tib_df( - "links", - tibblify::tib_chr("name", required = FALSE), - tibblify::tib_chr("url", required = FALSE) - ), - - tibblify::tib_df( - "logos", - tibblify::tib_chr("theme", required = FALSE), - tibblify::tib_df( - "formats", - tibblify::tib_chr("src", required = FALSE), - tibblify::tib_unspecified("background", required = FALSE), - tibblify::tib_chr("format", required = FALSE), - tibblify::tib_int("height", required = FALSE), - tibblify::tib_int("width", required = FALSE), - tibblify::tib_int("size", required = FALSE), - ), - tibblify::tib_unspecified("tags", required = FALSE), - tibblify::tib_chr("type", required = FALSE) - ), - - tibblify::tib_df( - "colors", - tibblify::tib_chr("hex", required = FALSE), - tibblify::tib_chr("type", required = FALSE), - tibblify::tib_int("brightness", required = FALSE), - ), - - tibblify::tib_df( - "fonts", - tibblify::tib_chr("name", required = FALSE), - tibblify::tib_chr("type", required = FALSE), - tibblify::tib_chr("origin", required = FALSE), - tibblify::tib_chr("originId", required = FALSE), - tibblify::tib_unspecified("weights", required = FALSE), - ), - - tibblify::tib_row( - "company", - tibblify::tib_unspecified("employees"), - tibblify::tib_unspecified("foundedYear"), - tibblify::tib_unspecified("kind"), - tibblify::tib_unspecified("location"), - tibblify::tib_df( - "industries", - tibblify::tib_unspecified("id", required = FALSE), - tibblify::tib_unspecified("parent"), - tibblify::tib_dbl("score", required = FALSE), - tibblify::tib_chr("name", required = FALSE), - tibblify::tib_chr("emoji", required = FALSE), - tibblify::tib_chr("slug", required = FALSE) - ) - ) - ) - - out <- tibblify::tibblify(content, spec, unspecified = "drop") - out$logos <- out$logos |> tidyr::unnest("formats") - out$company <- out$company |> purrr::pluck("industries") - - return(out) - -} - - -get_brand_logos <- function( - brand, - path, - ... -) { - - brand_logos <- brand$logos |> - dplyr::mutate( - file = purrr::pmap_chr( - list( - brand_name = brand$name, - type = type, - format = format, - height = height, - width = width - ), - get_logo_file_name - ) - ) - - purrr::walk2( - brand_logos$src, - brand_logos$file, - ~download_logo( - src = .x, - file = .y, - name = brand$name, - type = brand_logos$type, - format = brand_logos$format, - height = brand_logos$height, - width = brand_logos$width - ) - ) - - return( - invisible(TRUE) - ) - - - - -} - -#' Download Brand Logo File -#' -#' @description -#' This function downloads a brand logo file from a URL to the specified path. -#' -#' @param src The URL of the logo file -#' @param file The name of the logo file -#' @param name The name of the brand -#' @param path The path to save the logo file -#' @param type The type of logo (icon or logo) -#' @param format The format of the logo (png, svg, jpeg) -#' @param height The height of the logo -#' @param width The width of the logo -#' @param ... Additional arguments -#' -#' @return Invisible -#' @export -#' -#' @importFrom stringr str_replace_all str_to_lower -#' @importFrom fs dir_exists dir_create path -download_logo <- function( - src, - file, - name, - path = "inst/extdata/brand", - type = c("icon", "logo"), - format = c("png", "svg", "jpeg"), - height, - width, - ... -) { - - type <- match.arg(type) - format <- match.arg(format) - height <- as.integer(height) - width <- as.integer(width) - src <- src |> stringr::str_replace_all(" ", "%20") - brand_name_clean <- stringr::str_to_lower(name) |> stringr::str_replace_all(" ", "_") - size <- paste0(as.character(height), "x", as.character(width)) - - if (!fs::dir_exists(path)) { - fs::dir_create(path) - } - - file_path <- fs::path(path, file) - - download.file( - src, - destfile = file_path, - method = "curl" - ) - - return( - invisible(TRUE) - ) - -} - -#' @param brand_name The name of the brand -#' @param type The type of logo -#' @param format The format of the logo -#' @param height The height of the logo -#' @param width The width of the logo -#' @param ... Additional arguments -#' -#' @return The file name of the logo -#' -#' @export -#' -#' @noRd -#' -#' @keywords internal -#' -#' @importFrom stringr str_to_lower str_replace_all -#' @importFrom purrr pmap_chr -#' @importFrom dplyr mutate -#' @importFrom fs dir_exists dir_create path -get_logo_file_name <- function( - brand_name, - type, - format, - height, - width, - ... -) { - - brand_name_clean <- stringr::str_to_lower(brand_name) |> stringr::str_replace_all(" ", "_") - size <- "" - if (!is.na(height) && !is.na(width) && format != "svg") { - size <- paste0("-", as.character(height), "x", as.character(width)) - } - - paste0( - brand_name_clean, - "-", - type, - size, - ".", - format - ) - -} - -# brand_logos <- brand$logos |> -# dplyr::mutate( -# file = purrr::pmap_chr( -# list( -# brand_name = brand$name, -# type = type, -# format = format, -# height = height, -# width = width -# ), -# get_logo_file_name -# ) -# ) - - - - -# download_logo( -# src = brand_logos$src[3], -# file = brand_logos$file[3], -# name = brand$name, -# type = brand_logos$type[3], -# format = brand_logos$format[3], -# height = brand_logos$height[3], -# width = brand_logos$width[3] -# ) diff --git a/R/utils_colors.R b/R/utils_colors.R new file mode 100644 index 0000000..302065f --- /dev/null +++ b/R/utils_colors.R @@ -0,0 +1,71 @@ +#' Converts RGB values to hex colour code +#' +#' @param x A matrix of red, blue and green values +#' +#' @return A corresponding hex colour code +#' @export +#' +#' @examples temp_rgb_matrix <- rgba_to_rgb(c(52, 46, 39, 0.8)) +#' rgb_to_hex(temp_rgb_matrix) +#' +rgb_to_hex <- function(x){ + grDevices::rgb(x[1], x[2], x[3], maxColorValue = 255) +} + +#' Converts Hex codes values to RGB vectors +#' +#' @param x A hex colour code +#' +#' @return A corresponding matrix of red, blue and green values +#' @export +#' +#' @examples hex_to_rgb("purple") +#' hex_to_rgb("#fafafa") +#' +hex_to_rgb <- function(x){ + + temp_tibble <- as.data.frame(grDevices::col2rgb(x, alpha = FALSE)) + + paste0(c("r = ", "g = ", "b = "), temp_tibble$V1, + collapse = ", ") +} + +#' Convert RGB to HEX +#' +#' @param colour_rgba A vector of length 4: c(red value, green value, blue value, alpha). +#' All colour values must be between 0 and 255. Alpha must be between 0 and 1. +#' @param background_colour Defaults to white. Users can specify a different colour to get +#' the hex code for their original colour blended with a specified background colour. +#' `background_colour` must either be a recognised colour name (e.g. "white"), +#' a hex colour code (e.g. "#ffffff") or vector of length 3 (red value, green value, blue value), +#' with all values between 0 and 255. The default value is white ("#ffffff"). +#' @param ... Allows for US spelling of color/colour. +#' +#' @return Returns the corresponding hex colour code +#' @export +#' +#' @examples rgba_to_hex(c(52, 46, 39, 0.8)) +#' +#' rgba_to_hex(c(52, 46, 39, 0.8), "blue") +#' +#' rgba_to_hex(c(52, 46, 39, 0.8), "#032cfc") +rgba_to_hex <- function(colour_rgba, background_colour = "#ffffff", ...){ + + # Allows for US spelling input + check_dots <- list(...) + + if(missing(colour_rgba) & "color_rgba" %in% names(check_dots)) { + colour_rgba <- check_dots$color_rgba + } + + if("background_color" %in% names(check_dots)) { + background_colour <- check_dots$background_color + } + + new_col <- rgba_to_rgb(colour_rgba, background_colour) + + new_col_hex <- rgb_to_hex(new_col) + + return(new_col_hex) + +} diff --git a/R/utils_images.R b/R/utils_images.R new file mode 100644 index 0000000..f2ab73b --- /dev/null +++ b/R/utils_images.R @@ -0,0 +1,24 @@ +resize_crop_to_face <- function(image, size = 600) { + image <- resize_fit(image, size) + info <- image_info(image) + + # size may have changed after refit + size <- min(info$height, info$width) + + is_image_square <- info$width == info$height + if (is_image_square) { + return(image) + } + + face <- find_face_center(image) + + image_crop( + image, + geometry = geometry_area( + width = size, + height = size, + x_off = crop_offset(face$x, info$width, size), + y_off = crop_offset(face$y, info$height, size) + ) + ) +} diff --git a/R/utils_sys.R b/R/utils_sys.R new file mode 100644 index 0000000..0e1fb68 --- /dev/null +++ b/R/utils_sys.R @@ -0,0 +1,13 @@ +get_sys_path <- function() { + Sys.getenv("PATH") |> stringr::str_split(";") |> unlist() +} + +test_sys_path <- function(value) { + + if (Sys.which(value) == "") { + return(FALSE) + } + + return(TRUE) + +} diff --git a/R/utils_toggl.R b/R/utils_toggl.R new file mode 100644 index 0000000..25b42c7 --- /dev/null +++ b/R/utils_toggl.R @@ -0,0 +1,122 @@ + +# ------------------------------------------------------------------------ +# +# Title : Toggl Utilities +# By : Jimmy Briggs +# Date : 2024-09-05 +# +# ------------------------------------------------------------------------ + + +#' Toggl Time Tracking +#' +#' @name time_tracking +#' +#' @description +#' Functions for tracking time in the current project's context via Toggl. +#' +#' - `start_time_tracking()`: Start tracking time in Toggl. +#' - `stop_time_tracking()`: Stop tracking time in Toggl. +#' - `get_tracked_time()`: Retrieve tracked time entries from Toggl. +#' +#' @param description A description of the time entry. +#' Default is "R Development for GMH Leasing Dashboard" in this project. +#' @param tags A character vector of tags to apply to the time entry. +#' Note that if the project is billable, the "Billable" tag will be added. +#' @param config A configuration list for the Toggl project. +#' By default will retrieve values from the `toggl` configuration setup in +#' the `config.yml` for the project. +#' @param ... Additional arguments to pass to the various `togglr` functions. +#' +#' @return +#' - `start_time_tracking()`: The response from the Toggl API for starting time tracking. +#' - `stop_time_tracking()`: The response from the Toggl API for stopping time tracking. +#' - `get_tracked_time()`: A data frame of the time entries retrieved from Toggl. +NULL + +#' @rdname time_tracking +#' @export +#' @importFrom togglr toggl_start get_toggl_api_token set_toggl_api_token +#' @importFrom config get +#' @importFrom cli cli_bullets +start_time_tracking <- function( + description = "R Development for GMH Leasing Dashboard", + tags = c(), + config = config::get("toggl"), + ... +) { + + api_key <- togglr::get_toggl_api_token(ask = FALSE) + if (is.null(api_key)) { + api_key <- config$api_token + togglr::set_toggl_api_token(api_key) + } + + if (as.logical(config$is_billable) == TRUE) { + tags <- c("Billable", tags) + } + + res <- togglr::toggl_start( + description = description, + client = config$client_name, + project_name = config$project_name, + api_token = api_key, + tags = tags + ) + + cli::cli_bullets( + c( + "v" = "Time Tracking Started via Toggl (ID: {.field {res}})", + "i" = "Description: {.field {description}}", + "i" = "Client: {.field {config$client_name}}", + "i" = "Project: {.field {config$project_name}}", + "i" = "Tags: {.field {tags}}", + ">" = "To Stop Tracking run {.code stop_time_tracking()}" + ) + ) + + return(res) + +} + +#' @rdname time_tracking +#' @export +#' @importFrom togglr toggl_stop +#' @importFrom cli cli_abort cli_bullets +#' @importFrom rlang current_env +stop_time_tracking <- function(...) { + + call_env <- rlang::current_env() + + res <- tryCatch({ + togglr::toggl_stop(...) + }, error = function(e, call = call_env) { + cli::cli_abort( + c( + "Failed to stop time tracking via Toggl. Error: {.error {e}}", + "Are tou sure you started tracking time?" + ), + call = call + ) + }) + + cli::cli_bullets( + c( + "v" = "Time Tracking Stopped via Toggl", + ">" = "To view the time entry run {.code get_tracked_time()}" + ) + ) + +} + +#' @rdname time_tracking +#' @export +#' @importFrom lubridate weeks +#' @importFrom togglr get_time_entries +get_tracked_time <- function( + start = Sys.time() - lubridate::weeks(1), + end = Sys.time(), + ... +) { + togglr::get_time_entries(since = start, until = end, ...) +} diff --git a/README.Rmd b/README.Rmd index d4ee9f2..6935feb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -13,49 +13,41 @@ knitr::opts_chunk$set( ) ``` -# noclocksR +# No Clocks R Package - `noclocksr` No Clocks Logo +> [!NOTE] +> This package is under active development and is not yet ready for use. [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![Project Status: WIP](https://www.repostatus.org/badges/latest/wip.svg)](http://www.repostatus.org/#wip) -[![Generate CHANGELOG.md](https://github.com/noclocks/noclocksR/actions/workflows/changelog.yml/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/changelog.yml) -[![pkgdown](https://github.com/noclocks/noclocksR/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/pkgdown.yaml) -[![pages-build-deployment](https://github.com/noclocks/noclocksR/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/pages/pages-build-deployment) -The goal of noclocksR is to ... +## Contents -## Installation +- [Overview](#overview) +- [Installation](#installation) +- [Usage](#usage) +- [License](#license) -You can install the development version of noclocksR from [GitHub](https://github.com/) with: +## Overview -``` r -# install.packages("devtools") -devtools::install_github("noclocks/noclocksR") -``` +The `noclocksr` package is a collection of functions and datasets for use in No Clocks, LLC projects. -## Example +## Installation -This is a basic example which shows you how to solve a common problem: +You can install the development version of `noclocksr` from GitHub with: -```{r example} -# library(noclocksR) -## basic example code +```r +pak::pak("noclocks/noclocksr") ``` -What is special about using `README.Rmd` instead of just `README.md`? You can include R chunks like so: +## Usage -```{r cars} -summary(cars) -``` +The package is under active development and is not yet ready for use. -You'll still need to render `README.Rmd` regularly, to keep `README.md` up-to-date. `devtools::build_readme()` is handy for this. +## License -You can also embed plots, for example: +The `noclocksr` package is free and open source software, licensed under the [MIT License](LICENSE.md). -```{r pressure, echo = FALSE} -plot(pressure) -``` -In that case, don't forget to commit and push the resulting figure files, so they display on GitHub and CRAN. diff --git a/README.md b/README.md index 4fefed8..ee33a5f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# noclocksR +# No Clocks R Package - `noclocksr` No Clocks Logo + +> \[!NOTE\] This package is under active development and is not yet +> ready for use. @@ -9,53 +12,33 @@ experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![Project Status: WIP](https://www.repostatus.org/badges/latest/wip.svg)](http://www.repostatus.org/#wip) -[![Generate -CHANGELOG.md](https://github.com/noclocks/noclocksR/actions/workflows/changelog.yml/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/changelog.yml) -[![pkgdown](https://github.com/noclocks/noclocksR/actions/workflows/pkgdown.yaml/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/pkgdown.yaml) -[![pages-build-deployment](https://github.com/noclocks/noclocksR/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/noclocks/noclocksR/actions/workflows/pages/pages-build-deployment) -The goal of noclocksR is to â€Ļ - -## Installation +## Contents -You can install the development version of noclocksR from -[GitHub](https://github.com/) with: +- [Overview](#overview) +- [Installation](#installation) +- [Usage](#usage) +- [License](#license) -``` r -# install.packages("devtools") -devtools::install_github("noclocks/noclocksR") -``` +## Overview -## Example +The `noclocksr` package is a collection of functions and datasets for +use in No Clocks, LLC projects. -This is a basic example which shows you how to solve a common problem: - -``` r -# library(noclocksR) -## basic example code -``` +## Installation -What is special about using `README.Rmd` instead of just `README.md`? -You can include R chunks like so: +You can install the development version of `noclocksr` from GitHub with: ``` r -summary(cars) -#> speed dist -#> Min. : 4.0 Min. : 2.00 -#> 1st Qu.:12.0 1st Qu.: 26.00 -#> Median :15.0 Median : 36.00 -#> Mean :15.4 Mean : 42.98 -#> 3rd Qu.:19.0 3rd Qu.: 56.00 -#> Max. :25.0 Max. :120.00 +pak::pak("noclocks/noclocksr") ``` -You’ll still need to render `README.Rmd` regularly, to keep `README.md` -up-to-date. `devtools::build_readme()` is handy for this. +## Usage -You can also embed plots, for example: +The package is under active development and is not yet ready for use. - +## License -In that case, don’t forget to commit and push the resulting figure -files, so they display on GitHub and CRAN. +The `noclocksr` package is free and open source software, licensed under +the [MIT License](LICENSE.md). diff --git a/_pkgdown.yml b/_pkgdown.yml index a0117e0..5c3eda3 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,4 +1,4 @@ -url: https://docs.noclocks.dev/noclocksR/ +url: https://docs.noclocks.dev/noclocksr/ home: title: No Clocks Internal R Package authors: diff --git a/data-raw/brand.R b/data-raw/brand.R index e69de29..ad5bc62 100644 --- a/data-raw/brand.R +++ b/data-raw/brand.R @@ -0,0 +1,71 @@ +brandfetch_response_tspec <- tibblify::tspec_object( + tibblify::tib_chr("id", required = FALSE), + tibblify::tib_chr("name", required = FALSE), + tibblify::tib_chr("domain", required = FALSE), + tibblify::tib_lgl("claimed", required = FALSE), + tibblify::tib_chr("description", required = FALSE), + tibblify::tib_chr("longDescription", required = FALSE), + tibblify::tib_dbl("qualityScore", required = FALSE), + tibblify::tib_unspecified("images"), + + tibblify::tib_df( + "links", + tibblify::tib_chr("name", required = FALSE), + tibblify::tib_chr("url", required = FALSE) + ), + + tibblify::tib_df( + "logos", + tibblify::tib_chr("theme", required = FALSE), + tibblify::tib_df( + "formats", + tibblify::tib_chr("src", required = FALSE), + tibblify::tib_unspecified("background", required = FALSE), + tibblify::tib_chr("format", required = FALSE), + tibblify::tib_int("height", required = FALSE), + tibblify::tib_int("width", required = FALSE), + tibblify::tib_int("size", required = FALSE), + ), + tibblify::tib_unspecified("tags", required = FALSE), + tibblify::tib_chr("type", required = FALSE) + ), + + tibblify::tib_df( + "colors", + tibblify::tib_chr("hex", required = FALSE), + tibblify::tib_chr("type", required = FALSE), + tibblify::tib_int("brightness", required = FALSE), + ), + + tibblify::tib_df( + "fonts", + tibblify::tib_chr("name", required = FALSE), + tibblify::tib_chr("type", required = FALSE), + tibblify::tib_chr("origin", required = FALSE), + tibblify::tib_chr("originId", required = FALSE), + tibblify::tib_unspecified("weights", required = FALSE), + ), + + tibblify::tib_row( + "company", + tibblify::tib_unspecified("employees"), + tibblify::tib_unspecified("foundedYear"), + tibblify::tib_unspecified("kind"), + tibblify::tib_unspecified("location"), + tibblify::tib_df( + "industries", + tibblify::tib_unspecified("id", required = FALSE), + tibblify::tib_unspecified("parent"), + tibblify::tib_dbl("score", required = FALSE), + tibblify::tib_chr("name", required = FALSE), + tibblify::tib_chr("emoji", required = FALSE), + tibblify::tib_chr("slug", required = FALSE) + ) + ) +) + +usethis::use_data( + brandfetch_response_tspec, + overwrite = FALSE, + internal = TRUE +) diff --git a/dev/functions.R b/dev/functions.R index 53ab135..af8098f 100644 --- a/dev/functions.R +++ b/dev/functions.R @@ -1,3 +1,69 @@ new_function <- function( + name, + path = fs::path("R", glue::glue("{name}.R")), + title = stringr::str_to_title(stringr::str_replace(name, "_", " ")), + test = TRUE, + example = TRUE, + export = TRUE, + open = rlang::is_interactive() +) { + + # setup for test and example if necessary + if (test) { + test_path <- fs::path("tests", "testthat", glue::glue("test_{name}.R")) + usethis::use_test(name = name, open = FALSE) + } + + if (example) { + + example_path <- fs::path("examples", glue::glue("ex_{name}.R")) + example_roxy <- glue::glue("#' @example examples/ex_{name}.R\n") + example_content <- glue::glue( + "if (FALSE) {{\n\n", + " {name}()\n\n", + "}}\n" + ) + + if (!fs::dir_exists(fs::path("examples"))) { + fs::dir_create(fs::path("examples")) + cli::cli_alert_success("Created examples directory: {.path {fs::path('examples')}/}.") + } + + if (!fs::file_exists(example_path)) { + cat(example_content, file = example_path, sep = "\n") + cli::cli_alert_success("Created example file: {.path {example_path}}.") + } + + } + + # function skeleton + skeleton <- glue::glue( + "#' {title}", "\n", + "#'", "\n", + "#' @description", "\n", + "#'", " ...", "\n", + "#'", "\n", + "#' @param ... ...", "\n", + "#'", "\n", + "#' @return ...", "\n", + "#'", "\n", + if (export) { "#' @export\n" } else { "#' @keywords internal\n" }, + "#'", "\n", + if (example) { "#' @example examples/ex_{name}.R\n" }, + "{name} <- function(...) {{", + " ", + "}}" + ) + + # write to file + if (!fs::file_exists(path)) { + cat(skeleton, file = path) + cli::cli_alert_success("Created function file: {.path {path}}.") + } + + if (open) { + rstudioapi::navigateToFile(path) + } + +} -) diff --git a/dev/pdf_processing.R b/dev/pdf_processing.R new file mode 100644 index 0000000..fb5f2a2 --- /dev/null +++ b/dev/pdf_processing.R @@ -0,0 +1,488 @@ +#' Process PDF Invoices and Receipts +#' +#' @description +#' This function processes PDF invoices and receipts by extracting the content, +#' parsing the content, and saving the files to the output directory. +#' +#' @details +#' This function implements PDF extraction by extracting the PDF content via +#' [pdftools::pdf_text()] and parsing the extracted text into the following +#' components: +#' - Document Type (Receipt or Invoice) +#' - Date +#' - Company Name +#' - ID (Receipt or Invoice Number) +#' - New File Name (Formatted as `YYYY-MM-DD-Company-DocumentType-ID.pdf`) +#' +#' The PDF file is then copied and renamed using the new file name inside of the +#' specified output directory and archived in the specified archive directory. +#' +#' Logs of the processing are written to a log file. +#' +#' @param input_dir (Required) The directory containing the PDF files to process. +#' @param output_dir (Required) The directory to save the processed PDF files. +#' @param archive_dir (Optional) The directory to archive the processed PDF files. +#' @param log_file (Optional) The path to the log file. Default is `getOption("log_file")`, +#' and if that is not set it will default to the path `Logs/` in the specified +#' output directory. +#' @param ... Additional arguments +#' +#' @return A list of the processed PDF files in the output directory. +#' @export +#' +#' @importFrom fs dir_exists dir_info dir_ls path_dir path file_exists file_create +#' @importFrom cli cli_progress_bar cli_progress_update cli_progress_done +#' @importFrom assertthat assert_that +#' @importFrom stringr str_extract str_trim +#' @importFrom pdftools pdf_text pdf_data +#' @importFrom purrr map pluck +#' @importFrom glue glue +#' +#' @examples +#' \dontrun{ +#' process_pdfs( +#' input_dir = fs::path_package("noclocksr", "PDFs"), +#' output_dir = fs::path("output"), +#' archive_dir = fs::path("archive"), +#' log_file = fs::path("Logs", paste0(Sys.Date(), ".log")) +#' ) +#' } +#' +#' @seealso +#' [extract_pdf_content()], [parse_pdf_content()] +#' [pdftools::pdf_text()], [pdftools::pdf_data()] +process_pdfs <- function( + input_dir, + output_dir, + archive_dir = fs::path( + input_dir, + "Archive", + Sys.Date() + ), + log_file = getOption( + "log_file", + fs::path( + output_dir, + "Logs", + paste0( + Sys.Date(), + ".log" + ) + ) + ), + ... +) { + + if (!is.null(log_file)) { + if (!fs::file_exists(log_file)) { + if (!fs::dir_exists(fs::path_dir(log_file))) { + fs::dir_create(fs::path_dir(log_file), recurse = TRUE) + } + fs::file_create(log_file) + } + if (is.null(getOption(log_file))) { + options("log_file" = log_file) + } + } else { + cli::cli_warn("⚠ī¸ No log file specified. Logging to console only.") + } + + write_log( + message = "Setting up PDF Processing...", + log_lvl = "INFO", + event = "Start" + ) + + invisible( + assertthat::assert_that( + fs::dir_exists(input_dir), + nrow(fs::dir_info(input_dir, glob = "*.pdf")) > 0, + msg = "The input directory must exist and contain PDF files." + ) + ) + + if (!fs::dir_exists(output_dir)) { + fs::dir_create(output_dir, recurse = TRUE) + write_log( + message = glue::glue( + "Created output directory: {output_dir}" + ), + log_lvl = "INFO", + event = "Success" + ) + } + + pdf_files <- fs::dir_ls(input_dir, glob = "*.pdf") + + write_log( + message = glue::glue( + "Found {length(pdf_files)} PDF files in {input_dir}" + ), + log_lvl = "INFO", + event = "Search" + ) + + cli::cli_progress_bar( + name = "Processing PDFs", + # status = "Processing", + # type = "iterator", + total = length(pdf_files)#, + # format = "{cli::pb_spin} Processing PDF File {cli::pb_current}/{cli::pb_total} {cli::pb_percent}\n{cli::pb_bar}", + ) + + for (pdf_file in pdf_files) { + + # browser() + + write_log( + message = glue::glue( + "Processing {pdf_file}..." + ), + log_lvl = "INFO", + event = "Process" + ) + + pdf_content <- extract_pdf_content(pdf_file) + pdf_content_parsed <- tryCatch({ + parse_pdf_content(pdf_content) + }, error = function(e) { + message <- glue::glue( + "Error processing {pdf_file}: {e$message}" + ) + write_log( + message = message, + log_lvl = "ERROR", + event = "Error" + ) + next # Skip to the next iteration + }, finally = { + if (is.null(pdf_content_parsed)) { + next # Skip to the next iteration + } + }) + + output_file <- fs::path( + output_dir, + paste0(pdf_content_parsed$type, "s"), + pdf_content_parsed$company, + pdf_content_parsed$new_file_name + ) + + if (fs::file_exists(output_file)) { + write_log( + message = glue::glue( + "File {output_file} already exists. Overwriting..." + ), + log_lvl = "WARNING", + event = "Warning" + ) + } + + fs::dir_create( + dirname(output_file), + recurse = TRUE + ) + + fs::file_copy(pdf_file, output_file, overwrite = TRUE) + + write_log( + message = glue::glue( + "Processed {pdf_file} to {output_file}" + ), + log_lvl = "INFO", + event = "Success" + ) + + if (!fs::dir_exists(archive_dir)) { + fs::dir_create(archive_dir, recurse = TRUE) + } + + fs::file_move(pdf_file, fs::path(archive_dir, basename(pdf_file))) + + write_log( + message = glue::glue( + "Archived {pdf_file} to {archive_dir}" + ), + log_lvl = "INFO", + event = "Saved" + ) + + cli::cli_progress_update() + + write_log( + message = glue::glue( + "Finished processing {pdf_file}" + ), + log_lvl = "INFO", + event = "Stop" + ) + + } + + cli::cli_progress_done() + + invisible(fs::dir_info(output_dir, glob = "*.pdf")) + +} + +extract_pdf_content <- function(path, ...) { + + assertthat::assert_that( + fs::path_ext(path) == "pdf", + msg = "The file must be a PDF." + ) + + assertthat::assert_that( + fs::file_exists(path), + msg = "The file does not exist." + ) + + pdf_content <- pdftools::pdf_text(path) + + assertthat::assert_that( + !is.null(pdf_content), + class(pdf_content) == "character", + length(pdf_content) > 0, + stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] %in% c("Receipt", "Invoice"), + msg = "The extracted PDF content is not valid." + ) + + return(pdf_content) + +} + +parse_pdf_content <- function(pdf_content, ...) { + + assertthat::assert_that( + !is.null(pdf_content), + class(pdf_content) == "character", + length(pdf_content) > 0, + stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] %in% c("Receipt", "Invoice"), + msg = "The extracted PDF content is not valid." + ) + + # Determine document type (Receipt or Invoice) + doc_type <- stringr::str_extract(pdf_content, stringr::boundary("word"))[[1]] + assertthat::assert_that( + !is.null(doc_type), + msg = "The document type could not be determined." + ) + + # Extract date from the appropriate line using global regex pattern + date_raw <- stringr::str_extract( + pdf_content, + "(?<=Date paid |paid on |Date of issue |due )[A-Za-z]+ \\d{1,2}, \\d{4}" + ) + + assertthat::assert_that( + !is.na(date_raw), + !is.null(date_raw), + msg = "The date could not be extracted." + ) + + date <- format( + as.Date( + date_raw, + format = "%B %d, %Y" + ), + "%Y-%m-%d" + ) |> as.character() + + assertthat::assert_that( + !is.na(date), + !is.null(date), + nchar(date) == 10, + msg = "The date could not be formatted." + ) + + # Extract company name from the address block by finding the text right + # before "Bill to"\ + # example: "\nFly.io, Inc. Bill to\n2261 Market Street #4990" + # company: Fly.io, Inc. + company_line <- stringr::str_extract( + pdf_content, + "(?<=\\n)[A-Za-z\\. ]+(?=\\n\\d{4} |\\n\\d{3,5} )" + ) + + if (is.na(company_line)) { + company_line <- stringr::str_extract( + pdf_content, + "(?<=\\n)[A-Za-z\\.,\\s]+(?=\\s+Bill to)" + ) + } + + company_line <- company_line |> + stringr::str_trim() + + company <- stringr::str_extract( + company_line, + "[A-Za-z\\.]+" + ) + + assertthat::assert_that( + !is.na(company), + !is.null(company), + company != "", + msg = "Company name not found or invalid" + ) + + # Extract ID from the "Receipt" or "Invoice" number line + id <- ifelse( + doc_type == "Receipt", + stringr::str_extract( + pdf_content, + "(?<=Receipt number )[\\d\\-]+" + ), + stringr::str_extract( + pdf_content, + "(?<=Invoice number )[A-Z\\d\\-]+" + ) + ) + + assertthat::assert_that( + !is.na(id), + id != "", + msg = "ID not found or invalid" + ) + + new_file_name <- glue::glue( + "{date}-{company}-{doc_type}-{id}.pdf" + ) + + list( + type = doc_type, + date = date, + company = company, + id = id, + new_file_name = new_file_name + ) +} + +#' Write Log +#' +#' @description +#' Write log messages to the console and a log file. +#' +#' @param message (Required) Character string of the message to log. +#' @param log_file (Optional) The path to the log file. Default is `getOption("log_file")`. +#' @param log_lvl (Optional) The log level. Default is `INFO`. +#' @param event (Optional) The event type. Default is `Process`. +#' +#' @return Invisibly returns the log message. +#' @export +#' +#' @importFrom fs file_exists dir_exists dir_create file_create file_copy file_move +#' @importFrom cli cat_line cli_warn +#' @importFrom glue glue +write_log <- function(message, + log_file = getOption("log_file"), + log_lvl = "INFO", + event = "Process") { + + if (!is.null(log_file)) { + if (!fs::file_exists(log_file)) { + if (!fs::dir_exists(fs::path_dir(log_file))) { + fs::dir_create(fs::path_dir(log_file), recurse = TRUE) + } + fs::file_create(log_file) + } + } else { + cli::cli_warn("⚠ī¸ No log file specified. Logging to console only.") + } + + color <- switch( + log_lvl, + TRACE = "gray", + DEBUG = "orange", + INFO = "cyan", + WARNING = "yellow", + WARN = "yellow", + ERROR = "red", + FATAL = "maroon", + CRITICAL = "darkred", + "black" + ) + + event_sym <- switch( + event, + Start = "🚀", + Stop = "🛑", + Pause = "⏸", + Search = "🔍", + Process = "🔄", + Success = "✅", + Failure = "❌", + Saved = "💾", + Loaded = "📂", + Info = "ℹī¸", + Warning = "⚠ī¸", + Error = "❌", + Fatal = "💀", + Critical = "🚨", + "📝" + ) + + cli::cat_line( + glue::glue( + "{event_sym} [{log_lvl}]: {message}" + ), + col = color + ) + + msg <- glue::glue( + "\n{Sys.time()} [{log_lvl}]: {message}\n" + ) + + if (!is.null(log_file)) { + cat( + msg, + file = log_file, + append = TRUE, + sep = "\n" + ) + } +} + +extract_date_from_pdf <- function(pdf_content) { + hold <- stringr::str_extract( + pdf_content, + "(?<=paid on )\\w+ \\d{1,2}, \\d{4}" + ) + if (is.null(hold) || is.na(hold) || hold == "") { + hold <- stringr::str_extract( + pdf_content, + "(?<=Date paid )[A-Za-z]+ \\d{1,2}, \\d{4}" + ) + } + if (is.null(hold) || is.na(hold) || hold == "") { + hold <- stringr::str_extract( + pdf_content, + "(?<=Date: )\\w+ \\d{1,2}, \\d{4}" + ) + } +} + + +# test_pdf_receipt <- fs::path("inst/testfiles/Receipt-2217-5755 (2).pdf") +# test_pdf_invoice <- fs::path("inst/testfiles/Invoice-F091D19D-0002 (1).pdf") +# +# renamed_pdf_receipt <- "2024-05-08-Twenty.com-Receipt-2217-5775.pdf" +# renamed_pdf_invoice <- "2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf" +# +# test_pdf_receipt_text <- pdftools::pdf_text(test_pdf_receipt) +# test_pdf_invoice_text <- pdftools::pdf_text(test_pdf_invoice) +# +# test_pdf_receipt_data <- pdftools::pdf_data(test_pdf_receipt) |> purrr::pluck(1) +# test_pdf_invoice_data <- pdftools::pdf_data(test_pdf_invoice) |> purrr::pluck(1) +# +# test_pdf_receipt_type <- pdf_data$text[1] +# test_pdf_invoice_type <- pdf_data$text[1] +# +# test_pdf_receipt_date <- extract_date_from_pdf(test_pdf_receipt_text) +# test_pdf_invoice_date <- extract_date_from_pdf(test_pdf_invoice_text) + + + + + + diff --git a/dev/pkgdevt.R b/dev/pkgdevt.R index 7fc43b8..363950d 100644 --- a/dev/pkgdevt.R +++ b/dev/pkgdevt.R @@ -1,7 +1,7 @@ # ------------------------------------------------------------------------ # -# Title : noclocksR Package Development Script +# Title : noclocksr Package Development Script # By : Jimmy Briggs # Date : 2024-02-07 # @@ -31,7 +31,7 @@ usethis::use_data_raw("client_demo_data") # initialize -------------------------------------------------------------- -usethis::create_package("noclocksR") +usethis::create_package("noclocksr") usethis::use_namespace() usethis::use_roxygen_md() usethis::use_git() @@ -108,7 +108,7 @@ usethis::use_rmarkdown_template( ) -usethis::use_vignette("noclocksR") +usethis::use_vignette("noclocksr") usethis::use_vignette("styleguide") usethis::use_vignette("shiny") diff --git a/dev/vignettes.R b/dev/vignettes.R index 711564a..124d2b6 100644 --- a/dev/vignettes.R +++ b/dev/vignettes.R @@ -1,7 +1,7 @@ # ------------------------------------------------------------------------ # -# Title : noclocksR Package Vignettes +# Title : noclocksr Package Vignettes # By : Jimmy Briggs # Date : 2024-06-16 # @@ -17,7 +17,7 @@ require(rmarkdown) # vignettes --------------------------------------------------------------- c( - "noclocksR", + "noclocksr", "devenv", "pkgdevt", "shiny", @@ -30,3 +30,5 @@ c( purrr::walk( usethis::use_vignette ) + +usethis::use_vignette("naming-conventions") diff --git a/examples/ex_git_attributes.R b/examples/ex_git_attributes.R new file mode 100644 index 0000000..72b5fce --- /dev/null +++ b/examples/ex_git_attributes.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_attributes() + +} diff --git a/examples/ex_git_config.R b/examples/ex_git_config.R new file mode 100644 index 0000000..08fdbd5 --- /dev/null +++ b/examples/ex_git_config.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_config() + +} diff --git a/examples/ex_git_ignore.R b/examples/ex_git_ignore.R new file mode 100644 index 0000000..2f3d5e2 --- /dev/null +++ b/examples/ex_git_ignore.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_ignore() + +} diff --git a/examples/ex_shiny_resume.R b/examples/ex_shiny_resume.R index d49a085..d9a5140 100644 --- a/examples/ex_shiny_resume.R +++ b/examples/ex_shiny_resume.R @@ -1,5 +1,5 @@ library(shiny) -# library(noclocksR) +# library(noclocksr) ui <- function(request) { shiny_resume_page( diff --git a/inst/PDFs/Input/Invoice-7DF1CC31-0002.pdf b/inst/PDFs/Input/Invoice-7DF1CC31-0002.pdf new file mode 100644 index 0000000..4d32fbf Binary files /dev/null and b/inst/PDFs/Input/Invoice-7DF1CC31-0002.pdf differ diff --git a/inst/PDFs/Input/Invoice-7DF1CC31-0003.pdf b/inst/PDFs/Input/Invoice-7DF1CC31-0003.pdf new file mode 100644 index 0000000..342161f Binary files /dev/null and b/inst/PDFs/Input/Invoice-7DF1CC31-0003.pdf differ diff --git a/inst/PDFs/Input/Invoice-B85534E0-0001.pdf b/inst/PDFs/Input/Invoice-B85534E0-0001.pdf new file mode 100644 index 0000000..630c13a Binary files /dev/null and b/inst/PDFs/Input/Invoice-B85534E0-0001.pdf differ diff --git a/inst/PDFs/Input/Invoice-EE8D588C-0002 (1).pdf b/inst/PDFs/Input/Invoice-EE8D588C-0002 (1).pdf new file mode 100644 index 0000000..eba33d9 Binary files /dev/null and b/inst/PDFs/Input/Invoice-EE8D588C-0002 (1).pdf differ diff --git a/inst/PDFs/Input/Invoice-EE8D588C-0002 (2).pdf b/inst/PDFs/Input/Invoice-EE8D588C-0002 (2).pdf new file mode 100644 index 0000000..eba33d9 Binary files /dev/null and b/inst/PDFs/Input/Invoice-EE8D588C-0002 (2).pdf differ diff --git a/inst/PDFs/Input/Invoice-EE8D588C-0002 (3).pdf b/inst/PDFs/Input/Invoice-EE8D588C-0002 (3).pdf new file mode 100644 index 0000000..eba33d9 Binary files /dev/null and b/inst/PDFs/Input/Invoice-EE8D588C-0002 (3).pdf differ diff --git a/inst/PDFs/Input/Invoice-EE8D588C-0002.pdf b/inst/PDFs/Input/Invoice-EE8D588C-0002.pdf new file mode 100644 index 0000000..eba33d9 Binary files /dev/null and b/inst/PDFs/Input/Invoice-EE8D588C-0002.pdf differ diff --git a/inst/PDFs/Input/Invoice-EE8D588C-0003.pdf b/inst/PDFs/Input/Invoice-EE8D588C-0003.pdf new file mode 100644 index 0000000..880edad Binary files /dev/null and b/inst/PDFs/Input/Invoice-EE8D588C-0003.pdf differ diff --git a/inst/PDFs/Input/Invoice-F091D19D-0001.pdf b/inst/PDFs/Input/Invoice-F091D19D-0001.pdf new file mode 100644 index 0000000..3ac665c Binary files /dev/null and b/inst/PDFs/Input/Invoice-F091D19D-0001.pdf differ diff --git a/inst/PDFs/Input/Invoice-F091D19D-0002 (1).pdf b/inst/PDFs/Input/Invoice-F091D19D-0002 (1).pdf new file mode 100644 index 0000000..f4cea98 Binary files /dev/null and b/inst/PDFs/Input/Invoice-F091D19D-0002 (1).pdf differ diff --git a/inst/PDFs/Input/Invoice-F091D19D-0002.pdf b/inst/PDFs/Input/Invoice-F091D19D-0002.pdf new file mode 100644 index 0000000..e711a9d Binary files /dev/null and b/inst/PDFs/Input/Invoice-F091D19D-0002.pdf differ diff --git a/inst/PDFs/Input/Receipt-2217-5755 (1).pdf b/inst/PDFs/Input/Receipt-2217-5755 (1).pdf new file mode 100644 index 0000000..aa92b0e Binary files /dev/null and b/inst/PDFs/Input/Receipt-2217-5755 (1).pdf differ diff --git a/inst/PDFs/Input/Receipt-2217-5755 (2).pdf b/inst/PDFs/Input/Receipt-2217-5755 (2).pdf new file mode 100644 index 0000000..aa92b0e Binary files /dev/null and b/inst/PDFs/Input/Receipt-2217-5755 (2).pdf differ diff --git a/inst/PDFs/Input/Receipt-2217-5755.pdf b/inst/PDFs/Input/Receipt-2217-5755.pdf new file mode 100644 index 0000000..87d5837 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2217-5755.pdf differ diff --git a/inst/PDFs/Input/Receipt-2434-3640.pdf b/inst/PDFs/Input/Receipt-2434-3640.pdf new file mode 100644 index 0000000..8c0af29 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2434-3640.pdf differ diff --git a/inst/PDFs/Input/Receipt-2485-8071.pdf b/inst/PDFs/Input/Receipt-2485-8071.pdf new file mode 100644 index 0000000..a75ff86 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2485-8071.pdf differ diff --git a/inst/PDFs/Input/Receipt-2704-3284 (1).pdf b/inst/PDFs/Input/Receipt-2704-3284 (1).pdf new file mode 100644 index 0000000..46a5e02 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2704-3284 (1).pdf differ diff --git a/inst/PDFs/Input/Receipt-2704-3284 (2).pdf b/inst/PDFs/Input/Receipt-2704-3284 (2).pdf new file mode 100644 index 0000000..46a5e02 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2704-3284 (2).pdf differ diff --git a/inst/PDFs/Input/Receipt-2704-3284 (3).pdf b/inst/PDFs/Input/Receipt-2704-3284 (3).pdf new file mode 100644 index 0000000..46a5e02 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2704-3284 (3).pdf differ diff --git a/inst/PDFs/Input/Receipt-2704-3284.pdf b/inst/PDFs/Input/Receipt-2704-3284.pdf new file mode 100644 index 0000000..46a5e02 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2704-3284.pdf differ diff --git a/inst/PDFs/Input/Receipt-2704-9162.pdf b/inst/PDFs/Input/Receipt-2704-9162.pdf new file mode 100644 index 0000000..f7dc4f5 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2704-9162.pdf differ diff --git a/inst/PDFs/Input/Receipt-2881-1843.pdf b/inst/PDFs/Input/Receipt-2881-1843.pdf new file mode 100644 index 0000000..a64d166 Binary files /dev/null and b/inst/PDFs/Input/Receipt-2881-1843.pdf differ diff --git a/inst/PDFs/Logs/2024-06-25.log b/inst/PDFs/Logs/2024-06-25.log new file mode 100644 index 0000000..856dadc --- /dev/null +++ b/inst/PDFs/Logs/2024-06-25.log @@ -0,0 +1,133 @@ +2024-06-25 19:58:46.662744 [INFO]: Setting up PDF Processing... +2024-06-25 19:58:46.68227 [INFO]: Found 22 PDF files in inst/testfiles +2024-06-25 19:58:51.691017 [INFO]: Processing inst/testfiles/Invoice-7DF1CC31-0002.pdf... +2024-06-25 19:59:14.999441 [INFO]: Setting up PDF Processing... +2024-06-25 19:59:15.103628 [INFO]: Found 22 PDF files in inst/testfiles +2024-06-25 19:59:15.117707 [INFO]: Processing inst/testfiles/Invoice-7DF1CC31-0002.pdf... +2024-06-25 19:59:15.698131 [INFO]: Processing inst/testfiles/Invoice-7DF1CC31-0003.pdf... +2024-06-25 19:59:15.72441 [INFO]: Processing inst/testfiles/Invoice-B85534E0-0001.pdf... +2024-06-25 19:59:16.287901 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (1).pdf... +2024-06-25 19:59:16.310367 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (2).pdf... +2024-06-25 19:59:16.814305 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (3).pdf... +2024-06-25 19:59:16.83482 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002.pdf... +2024-06-25 19:59:16.856184 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0003.pdf... +2024-06-25 19:59:17.388135 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0001.pdf... +2024-06-25 19:59:17.409495 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0002 (1).pdf... +2024-06-25 19:59:17.918439 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0002.pdf... +2024-06-25 19:59:17.941641 [INFO]: Processing inst/testfiles/Receipt-2217-5755 (1).pdf... +2024-06-25 19:59:18.462188 [INFO]: Processing inst/testfiles/Receipt-2217-5755 (2).pdf... +2024-06-25 19:59:18.48377 [INFO]: Processing inst/testfiles/Receipt-2217-5755.pdf... +2024-06-25 19:59:18.505789 [INFO]: Processing inst/testfiles/Receipt-2434-3640.pdf... +2024-06-25 19:59:19.02697 [INFO]: Processing inst/testfiles/Receipt-2485-8071.pdf... +2024-06-25 19:59:19.049317 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (1).pdf... +2024-06-25 19:59:20.503627 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (2).pdf... +2024-06-25 19:59:20.52391 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (3).pdf... +2024-06-25 19:59:20.547248 [INFO]: Processing inst/testfiles/Receipt-2704-3284.pdf... +2024-06-25 19:59:20.569941 [INFO]: Processing inst/testfiles/Receipt-2704-9162.pdf... +2024-06-25 19:59:20.591426 [INFO]: Processing inst/testfiles/Receipt-2881-1843.pdf... +2024-06-25 20:00:09.825677 [INFO]: Setting up PDF Processing... +2024-06-25 20:00:09.848329 [INFO]: Found 22 PDF files in inst/testfiles +2024-06-25 20:00:09.863334 [INFO]: Processing inst/testfiles/Invoice-7DF1CC31-0002.pdf... +2024-06-25 20:00:10.019832 [INFO]: Processed inst/testfiles/Invoice-7DF1CC31-0002.pdf to inst/output/Invoices/Fly.io/2024-03-31-Fly.io-Invoice-7DF1CC31-0002.pdf +2024-06-25 20:00:10.040065 [INFO]: Archived inst/testfiles/Invoice-7DF1CC31-0002.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:10.537144 [INFO]: Finished processing inst/testfiles/Invoice-7DF1CC31-0002.pdf +2024-06-25 20:00:10.549376 [INFO]: Processing inst/testfiles/Invoice-7DF1CC31-0003.pdf... +2024-06-25 20:00:10.607084 [INFO]: Processed inst/testfiles/Invoice-7DF1CC31-0003.pdf to inst/output/Invoices/Fly.io/2024-04-30-Fly.io-Invoice-7DF1CC31-0003.pdf +2024-06-25 20:00:10.628507 [INFO]: Archived inst/testfiles/Invoice-7DF1CC31-0003.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:11.217802 [INFO]: Finished processing inst/testfiles/Invoice-7DF1CC31-0003.pdf +2024-06-25 20:00:11.231589 [INFO]: Processing inst/testfiles/Invoice-B85534E0-0001.pdf... +2024-06-25 20:00:11.257477 [INFO]: Processed inst/testfiles/Invoice-B85534E0-0001.pdf to inst/output/Invoices/Cal.com/2024-03-11-Cal.com-Invoice-B85534E0-0001.pdf +2024-06-25 20:00:13.302891 [INFO]: Archived inst/testfiles/Invoice-B85534E0-0001.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:13.371343 [INFO]: Finished processing inst/testfiles/Invoice-B85534E0-0001.pdf +2024-06-25 20:00:13.383048 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (1).pdf... +2024-06-25 20:00:13.446545 [INFO]: Processed inst/testfiles/Invoice-EE8D588C-0002 (1).pdf to inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf +2024-06-25 20:00:13.469247 [INFO]: Archived inst/testfiles/Invoice-EE8D588C-0002 (1).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:15.056749 [INFO]: Finished processing inst/testfiles/Invoice-EE8D588C-0002 (1).pdf +2024-06-25 20:00:15.07041 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (2).pdf... +2024-06-25 20:00:15.089942 [WARNING]: File inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf already exists. Overwriting... +2024-06-25 20:00:16.185519 [INFO]: Processed inst/testfiles/Invoice-EE8D588C-0002 (2).pdf to inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf +2024-06-25 20:00:16.203733 [INFO]: Archived inst/testfiles/Invoice-EE8D588C-0002 (2).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:17.771042 [INFO]: Finished processing inst/testfiles/Invoice-EE8D588C-0002 (2).pdf +2024-06-25 20:00:17.784152 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002 (3).pdf... +2024-06-25 20:00:17.80742 [WARNING]: File inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf already exists. Overwriting... +2024-06-25 20:00 + +:18.839792 [INFO]: Processed inst/testfiles/Invoice-EE8D588C-0002 (3).pdf to inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf +2024-06-25 20:00:18.856788 [INFO]: Archived inst/testfiles/Invoice-EE8D588C-0002 (3).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:18.926656 [INFO]: Finished processing inst/testfiles/Invoice-EE8D588C-0002 (3).pdf +2024-06-25 20:00:20.058373 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0002.pdf... +2024-06-25 20:00:20.078859 [WARNING]: File inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf already exists. Overwriting... +2024-06-25 20:00:20.097571 [INFO]: Processed inst/testfiles/Invoice-EE8D588C-0002.pdf to inst/output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf +2024-06-25 20:00:20.117287 [INFO]: Archived inst/testfiles/Invoice-EE8D588C-0002.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:20.189223 [INFO]: Finished processing inst/testfiles/Invoice-EE8D588C-0002.pdf +2024-06-25 20:00:20.20278 [INFO]: Processing inst/testfiles/Invoice-EE8D588C-0003.pdf... +2024-06-25 20:00:20.230052 [INFO]: Processed inst/testfiles/Invoice-EE8D588C-0003.pdf to inst/output/Invoices/Testimonial/2024-05-03-Testimonial-Invoice-EE8D588C-0003.pdf +2024-06-25 20:00:20.24928 [INFO]: Archived inst/testfiles/Invoice-EE8D588C-0003.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:20.264996 [INFO]: Finished processing inst/testfiles/Invoice-EE8D588C-0003.pdf +2024-06-25 20:00:20.281754 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0001.pdf... +2024-06-25 20:00:20.303347 [WARNING]: File inst/output/Invoices/Twenty.com/2024-05-01-Twenty.com-Invoice-F091D19D-0001.pdf already exists. Overwriting... +2024-06-25 20:00:20.351374 [INFO]: Processed inst/testfiles/Invoice-F091D19D-0001.pdf to inst/output/Invoices/Twenty.com/2024-05-01-Twenty.com-Invoice-F091D19D-0001.pdf +2024-06-25 20:00:21.842275 [INFO]: Archived inst/testfiles/Invoice-F091D19D-0001.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:21.906524 [INFO]: Finished processing inst/testfiles/Invoice-F091D19D-0001.pdf +2024-06-25 20:00:21.926093 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0002 (1).pdf... +2024-06-25 20:00:21.960865 [WARNING]: File inst/output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf already exists. Overwriting... +2024-06-25 20:00:23.130879 [INFO]: Processed inst/testfiles/Invoice-F091D19D-0002 (1).pdf to inst/output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf +2024-06-25 20:00:23.151666 [INFO]: Archived inst/testfiles/Invoice-F091D19D-0002 (1).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:23.223643 [INFO]: Finished processing inst/testfiles/Invoice-F091D19D-0002 (1).pdf +2024-06-25 20:00:23.238699 [INFO]: Processing inst/testfiles/Invoice-F091D19D-0002.pdf... +2024-06-25 20:00:23.260948 [WARNING]: File inst/output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf already exists. Overwriting... +2024-06-25 20:00:23.313058 [INFO]: Processed inst/testfiles/Invoice-F091D19D-0002.pdf to inst/output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf +2024-06-25 20:00:23.332097 [INFO]: Archived inst/testfiles/Invoice-F091D19D-0002.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:23.34662 [INFO]: Finished processing inst/testfiles/Invoice-F091D19D-0002.pdf +2024-06-25 20:00:23.362632 [INFO]: Processing inst/testfiles/Receipt-2217-5755 (1).pdf... +2024-06-25 20:00:23.394285 [WARNING]: File inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf already exists. Overwriting... +2024-06-25 20:00:25.443576 [INFO]: Processed inst/testfiles/Receipt-2217-5755 (1).pdf to inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf +2024-06-25 20:00:25.460992 [INFO]: Archived inst/testfiles/Receipt-2217-5755 (1).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:25.523782 [INFO]: Finished processing inst/testfiles/Receipt-2217-5755 (1).pdf +2024-06-25 20:00:25.535876 [INFO]: Processing inst/testfiles/Receipt-2217-5755 (2).pdf... +2024-06-25 20:00:25.55988 [WARNING]: File inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf already exists. Overwriting... +2024-06-25 20:00:26.629063 [INFO]: Processed inst/testfiles/Receipt-2217-5755 (2).pdf to inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf +2024-06-25 20:00:26.647744 [INFO]: Archived inst/testfiles/Receipt-2217-5755 (2).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:26.70954 [INFO]: Finished processing inst/testfiles/Receipt-2217-5755 (2).pdf +2024-06-25 20:00:28.291879 [INFO]: Processing inst/testfiles/Receipt-2217-5755.pdf... +2024-06-25 20:00:28.313173 [WARNING]: File inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf already exists. Overwriting... +2024-06-25 20:00:28.33622 [INFO]: Processed inst/testfiles/Receipt-2217-5755.pdf to inst/output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf +2024-06-25 20:00:29.324738 [INFO]: Archived inst/testfiles/Receipt-2217-5755.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:29.382672 [INFO]: Finished processing inst/testfiles/Receipt-2217-5755.pdf +2024-06-25 20:00:29.396803 [INFO]: Processing inst/testfiles/Receipt-2434-3640.pdf... +2024-06-25 20:00:29.451808 [INFO]: Processed inst/testfiles/Receipt-2434-3640.pdf + + to inst/output/Receipts/Fly.io/2024-04-30-Fly.io-Receipt-2434-3640.pdf +2024-06-25 20:00:29.470802 [INFO]: Archived inst/testfiles/Receipt-2434-3640.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:29.48476 [INFO]: Finished processing inst/testfiles/Receipt-2434-3640.pdf +2024-06-25 20:00:33.026432 [INFO]: Processing inst/testfiles/Receipt-2485-8071.pdf... +2024-06-25 20:00:33.059689 [INFO]: Processed inst/testfiles/Receipt-2485-8071.pdf to inst/output/Receipts/Fly.io/2024-03-31-Fly.io-Receipt-2485-8071.pdf +2024-06-25 20:00:33.07768 [INFO]: Archived inst/testfiles/Receipt-2485-8071.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:33.140303 [INFO]: Finished processing inst/testfiles/Receipt-2485-8071.pdf +2024-06-25 20:00:33.152349 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (1).pdf... +2024-06-25 20:00:33.193397 [INFO]: Processed inst/testfiles/Receipt-2704-3284 (1).pdf to inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf +2024-06-25 20:00:33.210892 [INFO]: Archived inst/testfiles/Receipt-2704-3284 (1).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:33.272968 [INFO]: Finished processing inst/testfiles/Receipt-2704-3284 (1).pdf +2024-06-25 20:00:34.76572 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (2).pdf... +2024-06-25 20:00:34.788251 [WARNING]: File inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf already exists. Overwriting... +2024-06-25 20:00:34.805841 [INFO]: Processed inst/testfiles/Receipt-2704-3284 (2).pdf to inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf +2024-06-25 20:00:34.826161 [INFO]: Archived inst/testfiles/Receipt-2704-3284 (2).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:35.986492 [INFO]: Finished processing inst/testfiles/Receipt-2704-3284 (2).pdf +2024-06-25 20:00:36.000903 [INFO]: Processing inst/testfiles/Receipt-2704-3284 (3).pdf... +2024-06-25 20:00:36.029981 [WARNING]: File inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf already exists. Overwriting... +2024-06-25 20:00:36.048859 [INFO]: Processed inst/testfiles/Receipt-2704-3284 (3).pdf to inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf +2024-06-25 20:00:36.066622 [INFO]: Archived inst/testfiles/Receipt-2704-3284 (3).pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:36.130394 [INFO]: Finished processing inst/testfiles/Receipt-2704-3284 (3).pdf +2024-06-25 20:00:36.14783 [INFO]: Processing inst/testfiles/Receipt-2704-3284.pdf... +2024-06-25 20:00:36.171819 [WARNING]: File inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf already exists. Overwriting... +2024-06-25 20:00:36.190132 [INFO]: Processed inst/testfiles/Receipt-2704-3284.pdf to inst/output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf +2024-06-25 20:00:36.208003 [INFO]: Archived inst/testfiles/Receipt-2704-3284.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:36.222345 [INFO]: Finished processing inst/testfiles/Receipt-2704-3284.pdf +2024-06-25 20:00:36.24009 [INFO]: Processing inst/testfiles/Receipt-2704-9162.pdf... +2024-06-25 20:00:36.265583 [INFO]: Processed inst/testfiles/Receipt-2704-9162.pdf to inst/output/Receipts/Testimonial/2024-05-03-Testimonial-Receipt-2704-9162.pdf +2024-06-25 20:00:40.052997 [INFO]: Archived inst/testfiles/Receipt-2704-9162.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:40.182052 [INFO]: Finished processing inst/testfiles/Receipt-2704-9162.pdf +2024-06-25 20:00:40.195756 [INFO]: Processing inst/testfiles/Receipt-2881-1843.pdf... +2024-06-25 20:00:40.704584 [INFO]: Processed inst/testfiles/Receipt-2881-1843.pdf to inst/output/Receipts/Cal.com/2024-03-11-Cal.com-Receipt-2881-1843.pdf +2024-06-25 20:00:40.725812 [INFO]: Archived inst/testfiles/Receipt-2881-1843.pdf to inst/testfiles/archive/2024-06-25 +2024-06-25 20:00:40.763891 [INFO]: Finished processing inst/testfiles/Receipt-2881-1843.pdf diff --git a/inst/PDFs/Output/Invoices/Cal.com/2024-03-11-Cal.com-Invoice-B85534E0-0001.pdf b/inst/PDFs/Output/Invoices/Cal.com/2024-03-11-Cal.com-Invoice-B85534E0-0001.pdf new file mode 100644 index 0000000..630c13a Binary files /dev/null and b/inst/PDFs/Output/Invoices/Cal.com/2024-03-11-Cal.com-Invoice-B85534E0-0001.pdf differ diff --git a/inst/PDFs/Output/Invoices/Fly.io/2024-03-31-Fly.io-Invoice-7DF1CC31-0002.pdf b/inst/PDFs/Output/Invoices/Fly.io/2024-03-31-Fly.io-Invoice-7DF1CC31-0002.pdf new file mode 100644 index 0000000..4d32fbf Binary files /dev/null and b/inst/PDFs/Output/Invoices/Fly.io/2024-03-31-Fly.io-Invoice-7DF1CC31-0002.pdf differ diff --git a/inst/PDFs/Output/Invoices/Fly.io/2024-04-30-Fly.io-Invoice-7DF1CC31-0003.pdf b/inst/PDFs/Output/Invoices/Fly.io/2024-04-30-Fly.io-Invoice-7DF1CC31-0003.pdf new file mode 100644 index 0000000..342161f Binary files /dev/null and b/inst/PDFs/Output/Invoices/Fly.io/2024-04-30-Fly.io-Invoice-7DF1CC31-0003.pdf differ diff --git a/inst/PDFs/Output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf b/inst/PDFs/Output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf new file mode 100644 index 0000000..eba33d9 Binary files /dev/null and b/inst/PDFs/Output/Invoices/Testimonial/2024-04-03-Testimonial-Invoice-EE8D588C-0002.pdf differ diff --git a/inst/PDFs/Output/Invoices/Testimonial/2024-05-03-Testimonial-Invoice-EE8D588C-0003.pdf b/inst/PDFs/Output/Invoices/Testimonial/2024-05-03-Testimonial-Invoice-EE8D588C-0003.pdf new file mode 100644 index 0000000..880edad Binary files /dev/null and b/inst/PDFs/Output/Invoices/Testimonial/2024-05-03-Testimonial-Invoice-EE8D588C-0003.pdf differ diff --git a/inst/PDFs/Output/Invoices/Twenty.com/2024-05-01-Twenty.com-Invoice-F091D19D-0001.pdf b/inst/PDFs/Output/Invoices/Twenty.com/2024-05-01-Twenty.com-Invoice-F091D19D-0001.pdf new file mode 100644 index 0000000..3ac665c Binary files /dev/null and b/inst/PDFs/Output/Invoices/Twenty.com/2024-05-01-Twenty.com-Invoice-F091D19D-0001.pdf differ diff --git a/inst/PDFs/Output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf b/inst/PDFs/Output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf new file mode 100644 index 0000000..e711a9d Binary files /dev/null and b/inst/PDFs/Output/Invoices/Twenty.com/2024-05-08-Twenty.com-Invoice-F091D19D-0002.pdf differ diff --git a/inst/PDFs/Output/Receipts/Cal.com/2024-03-11-Cal.com-Receipt-2881-1843.pdf b/inst/PDFs/Output/Receipts/Cal.com/2024-03-11-Cal.com-Receipt-2881-1843.pdf new file mode 100644 index 0000000..a64d166 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Cal.com/2024-03-11-Cal.com-Receipt-2881-1843.pdf differ diff --git a/inst/PDFs/Output/Receipts/Fly.io/2024-03-31-Fly.io-Receipt-2485-8071.pdf b/inst/PDFs/Output/Receipts/Fly.io/2024-03-31-Fly.io-Receipt-2485-8071.pdf new file mode 100644 index 0000000..a75ff86 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Fly.io/2024-03-31-Fly.io-Receipt-2485-8071.pdf differ diff --git a/inst/PDFs/Output/Receipts/Fly.io/2024-04-30-Fly.io-Receipt-2434-3640.pdf b/inst/PDFs/Output/Receipts/Fly.io/2024-04-30-Fly.io-Receipt-2434-3640.pdf new file mode 100644 index 0000000..8c0af29 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Fly.io/2024-04-30-Fly.io-Receipt-2434-3640.pdf differ diff --git a/inst/PDFs/Output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf b/inst/PDFs/Output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf new file mode 100644 index 0000000..46a5e02 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Testimonial/2024-04-03-Testimonial-Receipt-2704-3284.pdf differ diff --git a/inst/PDFs/Output/Receipts/Testimonial/2024-05-03-Testimonial-Receipt-2704-9162.pdf b/inst/PDFs/Output/Receipts/Testimonial/2024-05-03-Testimonial-Receipt-2704-9162.pdf new file mode 100644 index 0000000..f7dc4f5 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Testimonial/2024-05-03-Testimonial-Receipt-2704-9162.pdf differ diff --git a/inst/PDFs/Output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf b/inst/PDFs/Output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf new file mode 100644 index 0000000..87d5837 Binary files /dev/null and b/inst/PDFs/Output/Receipts/Twenty.com/2024-05-08-Twenty.com-Receipt-2217-5755.pdf differ diff --git a/R/assets.R b/inst/PDFs/README.md similarity index 100% rename from R/assets.R rename to inst/PDFs/README.md diff --git a/inst/config/.gitignore b/inst/config/.gitignore index 3427a25..6ed01a4 100644 --- a/inst/config/.gitignore +++ b/inst/config/.gitignore @@ -7,3 +7,6 @@ !keeper.config.template.json !oauth.template.json !service-account.template.json +!slack-app-manifest.yml +!slack-app-manifest.json +!slackr.example.dcf diff --git a/inst/config/README.md b/inst/config/README.md new file mode 100644 index 0000000..a89d84f --- /dev/null +++ b/inst/config/README.md @@ -0,0 +1,97 @@ +# Configurations + +## Integrations + +- Slack: + - Webhook URL + - Channel ID + - User ID + - App Manifest(s) +- Toggl: + - API Token + - Workspace ID + - Project ID + - Task ID + - User ID +- Todoist: + - API Token + - Project ID + - Task ID +- Raindrop.io: + - API Token + - Collection ID + - Bookmark ID +- Resend: + - API: + - API Key + - API Secret + - SMTP: + - SMTP Host + - SMTP Port + - SMTP User + - SMTP Password +- GitHub: + - Personal Access Token +- Supabase: + - Project ID + - API: + - ANON API Secret + - Service Role Key + - Database: + - URL + - Host + - Port + - User + - Password + - Database Name + - SSL Mode +- Keeper Password Manager: + - Server + - User + - Password + - Private Key + - Device Token +- Google Cloud Platform: + - Project ID + - API Key + - OAuth Client Credentials + - Service Account Credentials + - APIs: + - Authentication + - IAM + - Compute Engine + - Cloud Run + - Cloud Build + - Storage + - Scheduler + - Secret Manager + - Pub/Sub + - Maps APIs +- Google Workspace: + - Google Mail (gmail) + - Google Drive (gdrive) + - Google Calendar (gcal) + - Google Sheets (gsheets) + - Google Docs (gdocs) + - Google Meet (gmeet) + - Google Contacts (People API) + - Google Tasks +- Google Marketing Platform: + - Google My Business Profile: + - API Key + - Location ID + - Google Tag Manager: + - Container ID + - Google Ads: + - Customer ID + - Google Search Console: + - Property ID + - Google Analytics: + - Tracking ID +- Brandfetch: + - API Key +- RemoveBG: + - API Key +- Real Favicon Generator: + - API Key + diff --git a/inst/config/slack-app-manifest.yml b/inst/config/slack-app-manifest.yml new file mode 100644 index 0000000..14d753a --- /dev/null +++ b/inst/config/slack-app-manifest.yml @@ -0,0 +1,43 @@ +_metadata: + major_version: 1 + minor_version: 0 +display_information: + name: "No Clocks SlackR" + description: "Slack App for use with the `slackr` R Package" + background_color: "#000000" + long_description: "Slack App for use with the `slackr` R Package within the scope of the No Clocks slack workspace and channels. Features include but are not limited to managing channels, managing channel canvases, sending and receiving messages, etc."" +features: + bot_user: + display_name: slackr + always_online: false +oauth_config: + scopes: + bot: + - users:read + - channels:read + - channels:history + - channels:manage + - channels:write.invites + - files:read + - files:write + - groups:read + - groups:write + - links:read + - links:write + - chat:write + - chat:write.customize + - chat:write.public + - im:history + - im:write + - im:read + - bookmarks:read + - bookmarks:write + - canvases:read + - canvases:write + - app_mentions:read + - commands + - incoming-webhook +settings: + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false diff --git a/inst/rstudio/connections/local.R b/inst/rstudio/connections/local.R index d6600da..70a29d0 100644 --- a/inst/rstudio/connections/local.R +++ b/inst/rstudio/connections/local.R @@ -1,11 +1,11 @@ -library(noclocksR) +library(noclocksr) -noclocksR::set_config_file() -noclocksR::set_r_config("local") +noclocksr::set_config_file() +noclocksr::set_r_config("local") -db_config <- noclocksR::get_config("db") +db_config <- noclocksr::get_config("db") -connection <- noclocksR::db_connect( +connection <- noclocksr::db_connect( db_config = db_config, method = "DBI", rstudio_connection = TRUE diff --git a/inst/scripts/run_validation.R b/inst/scripts/run_validation.R new file mode 100644 index 0000000..1c9b581 --- /dev/null +++ b/inst/scripts/run_validation.R @@ -0,0 +1,5 @@ +pkg_name <- read.dcf("DESCRIPTION")[, "Package"] +pkg_version <- read.dcf("DESCRIPTION")[, "Version"] + +test_results <- tibble::as_tibble(devtools::test()) + diff --git a/inst/standalone/standalone-db_connect.R b/inst/standalone/standalone-db_connect.R index 66a00ee..7ee77bb 100644 --- a/inst/standalone/standalone-db_connect.R +++ b/inst/standalone/standalone-db_connect.R @@ -1,5 +1,5 @@ # --- -# repo: noclocks/noclocksR +# repo: noclocks/noclocksr # file: standalone-db_connect.R # last-updated: # license: https://unlicense.org diff --git a/inst/templates/github-workflows/changelog.yml.template b/inst/templates/github-workflows/changelog.yml.template new file mode 100644 index 0000000..b2433be --- /dev/null +++ b/inst/templates/github-workflows/changelog.yml.template @@ -0,0 +1,38 @@ +name: Automate Changelog +on: + workflow_dispatch: + workflow_call: + push: + branches: [ "main" ] + pull_request: +jobs: + changelog: + name: Generate Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: {{token}} + - name: Run Git Cliff + uses: tj-actions/git-cliff@v1.5.0 + id: git-cliff + with: + args: "--verbose" + output: "{{changelog_path}}" + template-config: "{{config_path}}" + - name: Print Changelog + id: print-changelog + run: | + cat "{{changelog_path}}" + # Commit and push the updated changelog, IF not a pull request + - name: Commit and Push Changelog + if: github.event_name != 'pull_request' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add {{changelog_path}} + git commit -m "[chore]: update changelog" + git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git "main" diff --git a/inst/templates/github-workflows/news.yml.template b/inst/templates/github-workflows/news.yml.template new file mode 100644 index 0000000..3540871 --- /dev/null +++ b/inst/templates/github-workflows/news.yml.template @@ -0,0 +1,52 @@ +name: Generate NEWS.md + +on: + push: + branches: + - main + - develop + workflow_dispatch: + +permissions: read-all + +jobs: + generate_changelog: + uses: ./.github/workflows/changelog.yml + generate_news: + needs: [generate_changelog] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: {{token}} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install Dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packges: any::pkgload, any::markdown, any::xml2, any::stringr + needs: pkgload + + - name: Generate NEWS.md + run: | + Rscript -e 'pkgload::load_all(); noclocksr::generate_news(output_file = "{{news_md_path}}", input_file = "{{changelog_path}}")' + + - name: Commit and push changes + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add {{news_md_path}} + git commit -m "docs: Update NEWS.md" || echo "No changes to commit" + git pull --ff-only + git push origin diff --git a/inst/vault/README b/inst/vault/README new file mode 100644 index 0000000..621db0b --- /dev/null +++ b/inst/vault/README @@ -0,0 +1 @@ +This directory is a secret vault. \ No newline at end of file diff --git a/inst/vault/secrets/README b/inst/vault/secrets/README new file mode 100644 index 0000000..42df4e3 --- /dev/null +++ b/inst/vault/secrets/README @@ -0,0 +1 @@ +This directory is part of a secret vault. \ No newline at end of file diff --git a/inst/vault/secrets/brandfetch_api_key/jimmy.briggs@noclocks.dev.enc b/inst/vault/secrets/brandfetch_api_key/jimmy.briggs@noclocks.dev.enc new file mode 100644 index 0000000..2e72c6e Binary files /dev/null and b/inst/vault/secrets/brandfetch_api_key/jimmy.briggs@noclocks.dev.enc differ diff --git a/inst/vault/secrets/brandfetch_api_key/secret.raw b/inst/vault/secrets/brandfetch_api_key/secret.raw new file mode 100644 index 0000000..6c5e91b Binary files /dev/null and b/inst/vault/secrets/brandfetch_api_key/secret.raw differ diff --git a/inst/vault/users/README b/inst/vault/users/README new file mode 100644 index 0000000..42df4e3 --- /dev/null +++ b/inst/vault/users/README @@ -0,0 +1 @@ +This directory is part of a secret vault. \ No newline at end of file diff --git a/inst/vault/users/jimmy.briggs@noclocks.dev.pem b/inst/vault/users/jimmy.briggs@noclocks.dev.pem new file mode 100644 index 0000000..128cd59 --- /dev/null +++ b/inst/vault/users/jimmy.briggs@noclocks.dev.pem @@ -0,0 +1,10 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPYfpA1byL8z1vLhuwEG +Z2BB5P2/eJZJy+nZ57gHaAhFMKP8f1OAOTV45EnxbHGHb5T0dSqa1TN5sOwhChYQ +xJMVGLh8M32fH05f5x6FTdboBQd5MXt3xr/zB9bJAUdOYePUrjsGww9y/xgPhMIw +J3ImerZHx4ZgeBSm9V4QFxqZkXkN1p/iyLhz6zlZtzEbBVZfDHkw1SuCPEaIHylU +nK4NwtDgeiVR747iNJ0qsXvpj976dDZN2sC+o2KSr/Pg3teo87i6OMwMhucFtqEd +V414vkanQULVNkwxX26G2WxNzbqxcTnY51gvczAzHXTLa4jraxv1Gy1rhkD8+itU +rwIDAQAB +-----END PUBLIC KEY----- + diff --git a/man/dot-onAttach.Rd b/man/dot-onAttach.Rd index d22ba9f..c065f03 100644 --- a/man/dot-onAttach.Rd +++ b/man/dot-onAttach.Rd @@ -4,7 +4,7 @@ \alias{.onAttach} \title{Startup Functions} \usage{ -.onAttach(libname = find.package("noclocksR"), pkgname = "noclocksR") +.onAttach(libname = find.package("noclocksr"), pkgname = "noclocksr") } \description{ These functions are run when the package is loaded or attached. diff --git a/man/download_brand_logos.Rd b/man/download_brand_logos.Rd new file mode 100644 index 0000000..383158f --- /dev/null +++ b/man/download_brand_logos.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/brandfetch.R +\name{download_brand_logos} +\alias{download_brand_logos} +\title{Download Brand Logos} +\usage{ +download_brand_logos(brand, path = "inst/extdata/brand", ...) +} +\arguments{ +\item{brand}{Brand} + +\item{path}{Path} + +\item{...}{...} +} +\value{ +Invisible +} +\description{ +Download Brand Logos +} diff --git a/man/download_logo.Rd b/man/download_logo.Rd index b33b719..4692787 100644 --- a/man/download_logo.Rd +++ b/man/download_logo.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_branding.R +% Please edit documentation in R/brandfetch.R \name{download_logo} \alias{download_logo} \title{Download Brand Logo File} diff --git a/man/extract_pdf_content.Rd b/man/extract_pdf_content.Rd new file mode 100644 index 0000000..c339676 --- /dev/null +++ b/man/extract_pdf_content.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pdf.R +\name{extract_pdf_content} +\alias{extract_pdf_content} +\title{Extract PDF Content} +\usage{ +extract_pdf_content(path, ...) +} +\arguments{ +\item{path}{(Required) The path to the PDF file.} + +\item{...}{Additional arguments} +} +\value{ +A character vector containing the text content of the PDF. +} +\description{ +Extracts the text content from a PDF file. +} diff --git a/man/fetch_brand.Rd b/man/fetch_brand.Rd index df07046..86a695a 100644 --- a/man/fetch_brand.Rd +++ b/man/fetch_brand.Rd @@ -1,16 +1,9 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/brandfetch.R, R/utils_branding.R +% Please edit documentation in R/brandfetch.R \name{fetch_brand} \alias{fetch_brand} \title{Fetch a Brand using the Brandfetch API} \usage{ -fetch_brand( - domain, - brandfetch_api_key = Sys.getenv("BRANDFETCH_API_KEY", unset = - config::get("brandfetch_api_key")), - ... -) - fetch_brand( domain, brandfetch_api_key = Sys.getenv("BRANDFETCH_API_KEY", unset = @@ -26,17 +19,9 @@ fetch_brand( \item{...}{Additional arguments} } \value{ -A tibble with the brand information - A tibble with the brand information } \description{ This function fetches a brand using the \href{https://docs.brandfetch.com/reference/brand-api}{Brandfetch Brand API}. - -This function fetches a brand using the -\href{https://docs.brandfetch.com/reference/brand-api}{Brandfetch Brand API}. -} -\examples{ - } diff --git a/man/figures/README-pressure-1.png b/man/figures/README-pressure-1.png deleted file mode 100644 index a3e0f2f..0000000 Binary files a/man/figures/README-pressure-1.png and /dev/null differ diff --git a/man/figures/logo.png b/man/figures/logo.png deleted file mode 100644 index b3c93a8..0000000 Binary files a/man/figures/logo.png and /dev/null differ diff --git a/man/figures/noclocks-logo.png b/man/figures/noclocks-logo.png new file mode 100644 index 0000000..235fb45 Binary files /dev/null and b/man/figures/noclocks-logo.png differ diff --git a/man/generate_news.Rd b/man/generate_news.Rd new file mode 100644 index 0000000..aff6341 --- /dev/null +++ b/man/generate_news.Rd @@ -0,0 +1,126 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pkg_news.R +\name{generate_news} +\alias{generate_news} +\alias{generate_news_from_changelog} +\title{Generate \code{NEWS.md}} +\usage{ +generate_news(output_file = "NEWS.md", ...) + +generate_news_from_changelog( + input_file = "CHANGELOG.md", + output_file = "NEWS.md", + include_unreleased = TRUE, + remove_commits = TRUE, + version_pattern = "^\\\\[(Unreleased|\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-\\\\w+)?)\\\\]", + ordered_groups = .ordered_groups, + skip_groups = NULL, + section_name_mapping = NULL, + verbose = TRUE, + overwrite = FALSE, + pkg_name = NULL, + pkg_version = NULL, + pkg_path = NULL +) +} +\arguments{ +\item{output_file}{Path to the output \code{NEWS.md} file.} + +\item{...}{Arguments passed on to \code{generate_news_from_changelog()} from +\code{generate_news()}.} + +\item{input_file}{Path to the \code{CHANGELOG.md} file.} + +\item{include_unreleased}{Logical indicating whether to include the +\verb{[Unreleased]} section (default: \code{TRUE}).} + +\item{remove_commits}{Logical indicating whether to remove commit hashes +and authors from the list items (default: \code{TRUE}).} + +\item{version_pattern}{Regular expression pattern to match version headers +(default: \verb{^\\\[(Unreleased|\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-\\\\w+)?)\\\]}).} + +\item{ordered_groups}{Character vector specifying the ordered groups for +sections in the \code{NEWS.md} file (default: \code{.ordered_groups}). +The default order is based on the significance of the groups.} + +\item{skip_groups}{Character vector specifying the groups to skip when +generating the \code{NEWS.md} file (default: \code{NULL}).} + +\item{section_name_mapping}{Named character vector to map section names +to custom names in the \code{NEWS.md} file (default: \code{NULL}). +The names should match the group names in the \code{CHANGELOG.md} file.} + +\item{verbose}{Logical indicating whether to display messages (default: \code{TRUE}).} + +\item{overwrite}{Logical indicating whether to overwrite existing \code{NEWS.md} file (default: \code{FALSE}).} + +\item{pkg_name}{Package name (default: \code{NULL}). If \code{NULL}, reads from \code{DESCRIPTION}.} + +\item{pkg_version}{Package version (default: \code{NULL}). If \code{NULL}, reads from \code{DESCRIPTION}.} + +\item{pkg_path}{Path to the package directory containing \code{DESCRIPTION} (default: \code{NULL}).} +} +\value{ +Both functions invisibly return the generated \code{news_content} +as a character vector. +} +\description{ +These functions generate the R package's \code{NEWS.md} file. +} +\details{ +\itemize{ +\item \code{generate_news()}: Generates a \code{NEWS.md} file by calling +\code{generate_news_from_changelog()} with default settings, +and will default to a generic \code{NEWS.md} file if no \code{CHANGELOG.md} +file is found. +\item \code{generate_news_from_changelog()}: Generates a \code{NEWS.md} file from a +pre-existing \code{CHANGELOG.md} file. It parses the Markdown content of +the \code{CHANGELOG.md} file, extracts version headers, sections, and their +content, and organizes them into a structured format suitable for +a typical R package's \code{NEWS.md} file. +} +} +\examples{ +if (interactive()) { + +# Examples of using the `generate_news()` function: +generate_news() + +# Examples of using the `generate_news_from_changelog()` function: + +# Generate NEWS.md from CHANGELOG.md using all default settings +generate_news_from_changelog() + +# Specify custom input and output files +generate_news_from_changelog( + input_file = "path/to/your/CHANGELOG.md", + output_file = "path/to/your/NEWS.md" +) + +# Overwrite the existing NEWS.md file +generate_news_from_changelog(overwrite = TRUE) + +# Exclude the 'Unreleased' section and keep commit hashes in the list items +generate_news_from_changelog(include_unreleased = FALSE, remove_commits = FALSE) + +# Skip certain sections +generate_news_from_changelog(skip_groups = c("Miscellaneous Tasks", "Meta")) + +# Map section names to custom names +generate_news_from_changelog( + section_name_mapping = c("Added" = "Features", "Fixed" = "Bug Fixes") +) + +# Use custom ordered groups +custom_ordered_groups <- c( + "Breaking Changes", "Features", "Bug Fixes", "Documentation", "Testing" +) +generate_news_from_changelog(ordered_groups = custom_ordered_groups) + +} +} +\seealso{ +\code{\link[=use_github_action_news]{use_github_action_news()}} for implementing this into a GitHub Action +Workflow. +} diff --git a/man/get_brand_logos.Rd b/man/get_brand_logos.Rd new file mode 100644 index 0000000..a50b366 --- /dev/null +++ b/man/get_brand_logos.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/brandfetch.R +\name{get_brand_logos} +\alias{get_brand_logos} +\title{Get Brand Logos} +\usage{ +get_brand_logos(brand, path, ...) +} +\arguments{ +\item{brand}{Brand} + +\item{path}{Path} + +\item{...}{...} +} +\value{ +Invisible +} +\description{ +Get Brand Logos +} diff --git a/man/get_logo_file_name.Rd b/man/get_logo_file_name.Rd new file mode 100644 index 0000000..293596a --- /dev/null +++ b/man/get_logo_file_name.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/brandfetch.R +\name{get_logo_file_name} +\alias{get_logo_file_name} +\title{Get Logo File Name} +\usage{ +get_logo_file_name( + brand_name, + type, + theme, + format, + height = NA, + width = NA, + ... +) +} +\arguments{ +\item{brand_name}{Brand Name} + +\item{type}{Type} + +\item{theme}{Theme} + +\item{format}{Format} + +\item{height}{Height} + +\item{width}{Width} + +\item{...}{...} +} +\value{ +The logo file name +} +\description{ +Get Logo File Name +} diff --git a/man/git_attributes.Rd b/man/git_attributes.Rd new file mode 100644 index 0000000..e2b353b --- /dev/null +++ b/man/git_attributes.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/git_attributes.R +\name{git_attributes} +\alias{git_attributes} +\title{Git Attributes} +\usage{ +git_attributes(...) +} +\arguments{ +\item{...}{...} +} +\value{ +... +} +\description{ +... +} +\examples{ +if (FALSE) { + + git_attributes() + +} +} diff --git a/man/git_config.Rd b/man/git_config.Rd new file mode 100644 index 0000000..b416b6c --- /dev/null +++ b/man/git_config.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/git_config.R +\name{git_config} +\alias{git_config} +\title{Git Config} +\usage{ +git_config(...) +} +\arguments{ +\item{...}{...} +} +\value{ +... +} +\description{ +... +} +\examples{ +if (FALSE) { + + git_config() + +} +} diff --git a/man/git_ignore.Rd b/man/git_ignore.Rd new file mode 100644 index 0000000..3db3e1c --- /dev/null +++ b/man/git_ignore.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/git_ignore.R +\name{git_ignore} +\alias{git_ignore} +\title{Git Ignore} +\usage{ +git_ignore(...) +} +\arguments{ +\item{...}{...} +} +\value{ +... +} +\description{ +... +} +\examples{ +if (FALSE) { + + git_ignore() + +} +} diff --git a/man/hex_to_rgb.Rd b/man/hex_to_rgb.Rd new file mode 100644 index 0000000..4ba0d3a --- /dev/null +++ b/man/hex_to_rgb.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_colors.R +\name{hex_to_rgb} +\alias{hex_to_rgb} +\title{Converts Hex codes values to RGB vectors} +\usage{ +hex_to_rgb(x) +} +\arguments{ +\item{x}{A hex colour code} +} +\value{ +A corresponding matrix of red, blue and green values +} +\description{ +Converts Hex codes values to RGB vectors +} +\examples{ +hex_to_rgb("purple") +hex_to_rgb("#fafafa") + +} diff --git a/man/noclocksR-package.Rd b/man/noclocksR-package.Rd deleted file mode 100644 index cb3ab9b..0000000 --- a/man/noclocksR-package.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/noclocksR-package.R -\docType{package} -\name{noclocksR-package} -\alias{noclocksR} -\alias{noclocksR-package} -\title{noclocksR: What the Package Does (One Line, Title Case)} -\description{ -\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} - -No Clocks, LLC packaged assets and workflows -} -\seealso{ -Useful links: -\itemize{ - \item \url{https://noclocks.github.io/noclocksR/} - \item \url{https://docs.noclocks.dev/noclocksR/} -} - -} -\author{ -\strong{Maintainer}: Jimmy Briggs \email{jimmy.briggs@jimbrig.com} (\href{https://orcid.org/0000-0002-7489-8787}{ORCID}) - -} -\keyword{internal} diff --git a/man/noclocksr-package.Rd b/man/noclocksr-package.Rd new file mode 100644 index 0000000..826b977 --- /dev/null +++ b/man/noclocksr-package.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/noclocksr-package.R +\docType{package} +\name{noclocksr-package} +\alias{noclocksr} +\alias{noclocksr-package} +\title{noclocksr: Internal Development at No Clocks, LLC} +\description{ +No Clocks, LLC packaged assets and workflows +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://noclocks.github.io/noclocksr/} + \item \url{https://docs.noclocks.dev/noclocksr/} +} + +} +\author{ +\strong{Maintainer}: Jimmy Briggs \email{jimmy.briggs@jimbrig.com} (\href{https://orcid.org/0000-0002-7489-8787}{ORCID}) + +} +\keyword{internal} diff --git a/man/parse_pdf_content.Rd b/man/parse_pdf_content.Rd new file mode 100644 index 0000000..5feb44f --- /dev/null +++ b/man/parse_pdf_content.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pdf.R +\name{parse_pdf_content} +\alias{parse_pdf_content} +\title{Parse PDF Content} +\usage{ +parse_pdf_content(pdf_content, ...) +} +\arguments{ +\item{pdf_content}{(Required) The text content extracted from the PDF file.} + +\item{...}{Additional arguments} +} +\value{ +A list containing parsed details: type, date, company, id, new_file_name. +} +\description{ +Parses the extracted text content from a PDF file. +} diff --git a/man/process_pdfs.Rd b/man/process_pdfs.Rd new file mode 100644 index 0000000..40718fa --- /dev/null +++ b/man/process_pdfs.Rd @@ -0,0 +1,70 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pdf.R +\name{process_pdfs} +\alias{process_pdfs} +\title{Process PDF Invoices and Receipts} +\usage{ +process_pdfs( + input_dir, + output_dir, + archive_dir = fs::path(input_dir, "archive", Sys.Date()), + log_file = getOption("log_file", fs::path(output_dir, "Logs", paste0(Sys.Date(), + ".log"))), + ... +) +} +\arguments{ +\item{input_dir}{(Required) The directory containing the PDF files to process.} + +\item{output_dir}{(Required) The directory to save the processed PDF files.} + +\item{archive_dir}{(Optional) The directory to archive the processed PDF files.} + +\item{log_file}{(Optional) The path to the log file. Default is \code{getOption("log_file")}, +and if that is not set it will default to the path \verb{Logs/} in the specified +output directory.} + +\item{...}{Additional arguments} +} +\value{ +A list of the processed PDF files in the output directory. +} +\description{ +This function processes PDF invoices and receipts by extracting the content, +parsing the content, and saving the files to the output directory. +} +\details{ +This function implements PDF extraction by extracting the PDF content via +\code{\link[pdftools:pdftools]{pdftools::pdf_text()}} and parsing the extracted text into the following +components: +\itemize{ +\item Document Type (Receipt or Invoice) +\item Date +\item Company Name +\item ID (Receipt or Invoice Number) +\item New File Name (Formatted as \code{YYYY-MM-DD-Company-DocumentType-ID.pdf}) +} + +The PDF file is then copied and renamed using the new file name inside of the +specified output directory and archived in the specified archive directory. + +Logs of the processing are written to a log file. +} +\examples{ +\dontrun{ + +fs::dir_copy(fs::path_package("noclocksr", "PDFs", "Input"), fs::path("Input")) + +process_pdfs( + input_dir = , + output_dir = fs::path("Output"), + archive_dir = fs::path("Archive"), + log_file = fs::path("Logs", paste0(Sys.Date(), ".log")) +) +} + +} +\seealso{ +\code{\link[=extract_pdf_content]{extract_pdf_content()}}, \code{\link[=parse_pdf_content]{parse_pdf_content()}} +\code{\link[pdftools:pdftools]{pdftools::pdf_text()}}, \code{\link[pdftools:pdftools]{pdftools::pdf_data()}} +} diff --git a/man/rgb_to_hex.Rd b/man/rgb_to_hex.Rd new file mode 100644 index 0000000..f268984 --- /dev/null +++ b/man/rgb_to_hex.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_colors.R +\name{rgb_to_hex} +\alias{rgb_to_hex} +\title{Converts RGB values to hex colour code} +\usage{ +rgb_to_hex(x) +} +\arguments{ +\item{x}{A matrix of red, blue and green values} +} +\value{ +A corresponding hex colour code +} +\description{ +Converts RGB values to hex colour code +} +\examples{ +temp_rgb_matrix <- rgba_to_rgb(c(52, 46, 39, 0.8)) +rgb_to_hex(temp_rgb_matrix) + +} diff --git a/man/rgba_to_hex.Rd b/man/rgba_to_hex.Rd new file mode 100644 index 0000000..ec0a7f9 --- /dev/null +++ b/man/rgba_to_hex.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_colors.R +\name{rgba_to_hex} +\alias{rgba_to_hex} +\title{Convert RGB to HEX} +\usage{ +rgba_to_hex(colour_rgba, background_colour = "#ffffff", ...) +} +\arguments{ +\item{colour_rgba}{A vector of length 4: c(red value, green value, blue value, alpha). +All colour values must be between 0 and 255. Alpha must be between 0 and 1.} + +\item{background_colour}{Defaults to white. Users can specify a different colour to get +the hex code for their original colour blended with a specified background colour. +\code{background_colour} must either be a recognised colour name (e.g. "white"), +a hex colour code (e.g. "#ffffff") or vector of length 3 (red value, green value, blue value), +with all values between 0 and 255. The default value is white ("#ffffff").} + +\item{...}{Allows for US spelling of color/colour.} +} +\value{ +Returns the corresponding hex colour code +} +\description{ +Convert RGB to HEX +} +\examples{ +rgba_to_hex(c(52, 46, 39, 0.8)) + +rgba_to_hex(c(52, 46, 39, 0.8), "blue") + +rgba_to_hex(c(52, 46, 39, 0.8), "#032cfc") +} diff --git a/man/time_tracking.Rd b/man/time_tracking.Rd new file mode 100644 index 0000000..4ea09e0 --- /dev/null +++ b/man/time_tracking.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_toggl.R +\name{time_tracking} +\alias{time_tracking} +\alias{start_time_tracking} +\alias{stop_time_tracking} +\alias{get_tracked_time} +\title{Toggl Time Tracking} +\usage{ +start_time_tracking( + description = "R Development for GMH Leasing Dashboard", + tags = c(), + config = config::get("toggl"), + ... +) + +stop_time_tracking(...) + +get_tracked_time( + start = Sys.time() - lubridate::weeks(1), + end = Sys.time(), + ... +) +} +\arguments{ +\item{description}{A description of the time entry. +Default is "R Development for GMH Leasing Dashboard" in this project.} + +\item{tags}{A character vector of tags to apply to the time entry. +Note that if the project is billable, the "Billable" tag will be added.} + +\item{config}{A configuration list for the Toggl project. +By default will retrieve values from the \code{toggl} configuration setup in +the \code{config.yml} for the project.} + +\item{...}{Additional arguments to pass to the various \code{togglr} functions.} +} +\value{ +\itemize{ +\item \code{start_time_tracking()}: The response from the Toggl API for starting time tracking. +\item \code{stop_time_tracking()}: The response from the Toggl API for stopping time tracking. +\item \code{get_tracked_time()}: A data frame of the time entries retrieved from Toggl. +} +} +\description{ +Functions for tracking time in the current project's context via Toggl. +\itemize{ +\item \code{start_time_tracking()}: Start tracking time in Toggl. +\item \code{stop_time_tracking()}: Stop tracking time in Toggl. +\item \code{get_tracked_time()}: Retrieve tracked time entries from Toggl. +} +} diff --git a/man/use_github_action_news.Rd b/man/use_github_action_news.Rd new file mode 100644 index 0000000..d24f92e --- /dev/null +++ b/man/use_github_action_news.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pkg_news.R +\name{use_github_action_news} +\alias{use_github_action_news} +\title{Generate GitHub Action Workflow for NEWS.md Generation} +\usage{ +use_github_action_news( + file_name = "news.yml", + news_md_path = "NEWS.md", + changelog_path = "CHANGELOG.md", + overwrite = TRUE, + verbose = TRUE +) +} +\arguments{ +\item{file_name}{Name of the output workflow file (default: \code{news.yml}).} + +\item{changelog_path}{Path to the \code{CHANGELOG.md} file (default: \code{CHANGELOG.md}).} + +\item{overwrite}{Logical indicating whether to overwrite the existing workflow file (default: \code{TRUE}).} + +\item{verbose}{Logical indicating whether to display messages (default: \code{TRUE}).} + +\item{config_path}{Path to the \code{cliff.toml} configuration file (default: \code{.github/cliff.toml}).} +} +\value{ +Invisibly returns \code{NULL}. +} +\description{ +This function generates a GitHub Action workflow YAML file that automates +the generation of \code{NEWS.md} from \code{CHANGELOG.md} whenever changes are pushed +to the repository. +} +\examples{ +if (interactive()) { + generate_github_action_workflow() +} +} diff --git a/man/write_log.Rd b/man/write_log.Rd new file mode 100644 index 0000000..3ace7b7 --- /dev/null +++ b/man/write_log.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pdf.R +\name{write_log} +\alias{write_log} +\title{Write Log} +\usage{ +write_log( + message, + log_file = getOption("log_file"), + log_lvl = "INFO", + event = "Process" +) +} +\arguments{ +\item{message}{(Required) Character string of the message to log.} + +\item{log_file}{(Optional) The path to the log file. Default is \code{getOption("log_file")}.} + +\item{log_lvl}{(Optional) The log level. Default is \code{INFO}.} + +\item{event}{(Optional) The event type. Default is \code{Process}.} +} +\value{ +Invisibly returns the log message. +} +\description{ +Write log messages to the console and a log file. +} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index de67eec..18748b3 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -1,4 +1,4 @@ -url: https:/docs.noclocks.dev/noclocksR/ +url: https:/docs.noclocks.dev/noclocksr/ home: title: "No Clocks Internal R Package" diff --git a/tests/testthat.R b/tests/testthat.R index 3a00f25..d90c146 100644 --- a/tests/testthat.R +++ b/tests/testthat.R @@ -7,6 +7,6 @@ # * https://testthat.r-lib.org/articles/special-files.html library(testthat) -library(noclocksR) +library(noclocksr) -test_check("noclocksR") +test_check("noclocksr") diff --git a/tests/testthat/_snaps/pkg_news.md b/tests/testthat/_snaps/pkg_news.md new file mode 100644 index 0000000..c5e792c --- /dev/null +++ b/tests/testthat/_snaps/pkg_news.md @@ -0,0 +1,10 @@ +# generate_news_from_changelog handles missing DESCRIPTION + + Code + generate_news_from_changelog(input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, verbose = FALSE, pkg_path = tmp_dir) + Condition + Error in `generate_news_from_changelog()`: + ! Output file already exists: {.path {output_file}}. + * Use `overwrite = TRUE` to overwrite. + diff --git a/tests/testthat/test-git_attributes.R b/tests/testthat/test-git_attributes.R new file mode 100644 index 0000000..8849056 --- /dev/null +++ b/tests/testthat/test-git_attributes.R @@ -0,0 +1,3 @@ +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) diff --git a/tests/testthat/test-git_config.R b/tests/testthat/test-git_config.R new file mode 100644 index 0000000..8849056 --- /dev/null +++ b/tests/testthat/test-git_config.R @@ -0,0 +1,3 @@ +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) diff --git a/tests/testthat/test-git_ignore.R b/tests/testthat/test-git_ignore.R new file mode 100644 index 0000000..8849056 --- /dev/null +++ b/tests/testthat/test-git_ignore.R @@ -0,0 +1,3 @@ +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) diff --git a/tests/testthat/test-pkg_news.R b/tests/testthat/test-pkg_news.R new file mode 100644 index 0000000..6267985 --- /dev/null +++ b/tests/testthat/test-pkg_news.R @@ -0,0 +1,102 @@ +test_that("generate_news_from_changelog works with default parameters", { + # Create a temporary directory + tmp_dir <- tempdir() + + # Write a sample CHANGELOG.md + changelog_content <- c( + "# Changelog", + "", + "All notable changes to this project will be documented in this file.", + "", + "## [Unreleased]", + "", + "### Added", + "- New feature A", + "- New feature B", + "", + "### Fixed", + "- Bug fix 1", + "- Bug fix 2", + "", + "## [1.0.1] - 2023-09-14", + "", + "### Fixed", + "- Minor bug fix", + "", + "## [1.0.0] - 2023-09-13", + "", + "### Added", + "- Initial release", + "" + ) + writeLines(changelog_content, file.path(tmp_dir, "CHANGELOG.md")) + + # Write a sample DESCRIPTION file + description_content <- c( + "Package: testpackage", + "Type: Package", + "Title: Test Package", + "Version: 1.0.0", + "Authors@R: person('First', 'Last', email = 'first.last@example.com', role = c('aut', 'cre'))", + "Description: A test package.", + "License: MIT" + ) + writeLines(description_content, file.path(tmp_dir, "DESCRIPTION")) + + # Call the function + output_file <- file.path(tmp_dir, "NEWS.md") + generate_news_from_changelog( + input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, + verbose = FALSE, + pkg_path = tmp_dir, + overwrite = TRUE + ) + + # Check that the NEWS.md file was created + expect_true(file.exists(output_file)) + + # Read the NEWS.md content + news_content <- readLines(output_file) + + # Check that the content contains expected entries + expect_true(any(grepl("# testpackage Unreleased", news_content))) + expect_true(any(grepl("## Added", news_content))) + expect_true(any(grepl("\\* New feature A", news_content))) + + # Clean up + unlink(tmp_dir) +}) + +test_that("generate_news_from_changelog handles missing DESCRIPTION", { + # Create a temporary directory + tmp_dir <- tempdir() + + # Write a sample CHANGELOG.md + changelog_content <- c( + "## [1.0.0] - 2023-09-13", + "", + "### Added", + "- Initial release", + "" + ) + writeLines(changelog_content, file.path(tmp_dir, "CHANGELOG.md")) + + # Call the function without a DESCRIPTION file + output_file <- file.path(tmp_dir, "NEWS.md") + + expect_snapshot( + x = { + generate_news_from_changelog( + input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, + verbose = FALSE, + pkg_path = tmp_dir + ) + }, + error = TRUE + ) + + # Clean up + unlink(tmp_dir) +}) diff --git a/vignettes/devenv.Rmd b/vignettes/devenv.Rmd index 792b530..4cd5f36 100644 --- a/vignettes/devenv.Rmd +++ b/vignettes/devenv.Rmd @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ``` diff --git a/vignettes/integrations.Rmd b/vignettes/integrations.Rmd index 6297f2d..c71997f 100644 --- a/vignettes/integrations.Rmd +++ b/vignettes/integrations.Rmd @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ``` diff --git a/vignettes/naming-conventions.Rmd b/vignettes/naming-conventions.Rmd new file mode 100644 index 0000000..33c9f47 --- /dev/null +++ b/vignettes/naming-conventions.Rmd @@ -0,0 +1,146 @@ +--- +title: "Naming Conventions" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Naming Conventions} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} + +``` + +# Naming Conventions in R + +> *Note* +> + +## File Naming Conventions + +### R Files + +Files containing R code (i.e. `*.R`) should be named using the following guidelines: + +- Use lowercase letters and underscores, also known as `snake_case`. + +- Use a descriptive name that clearly indicates the purpose of the file. + +- Use a *"noun_verb"* syntax to clearly indicate the purpose of the primary object + the code within the file interacts with. This will help in grouping related functions + and code while also making it easier to navigate the codebase. + +#### Example: Git Functions and Files + +For example, in this package, we provide a variety of functions for working with +`git`. These functions are stored inside R files under the `R/` folder using the +naming syntax `git_*.R`. For example, `git_config.R`, `git_ignore.R`, `git_attributes.R`, +and so on. + +The complete set of git R files and their corresponding exported functions are +as follows: + +- `git_config.R`: + - `git_config_get()` + - `git_config_set()` + - `git_config_unset()` + - `git_config_list()` + - `git_config_assert()` + - `git_config_path()` + - `git_config_edit()` + +- `git_ignore.R`: + - `git_ignore()` (alias for `git_ignore_add()`) + - `git_ignore_add()` + - `git_ignore_remove()` + - `git_ignore_list()` + - `git_ignore_assert()` + - `git_ignore_path()` + - `git_ignore_edit()` + +- `git_attributes.R`: + - `git_attributes()` (alias for `git_attributes_add()`) + - `git_attributes_add()` + - `git_attributes_remove()` + - `git_attributes_list()` + - `git_attributes_assert()` + - `git_attributes_path()` + - `git_attributes_edit()` + +- `git_hooks.R`: + - `git_hooks()` (alias for `git_hooks_add()`) + - `git_hooks_add()` + - `git_hooks_remove()` + - `git_hooks_list()` + - `git_hooks_assert()` + - `git_hooks_path()` + - `git_hooks_edit()` + +- `git_init.R`: + - `git_init()` + +You can see that the naming convention `git_*.R` helps in grouping related functions +together and makes it easier to navigate the codebase. + +### Vignettes and RMarkdown + +Vignettes and RMarkdown files should be named using the following guidelines: + +### Data Files + +Data files should be named using the following guidelines: + +### Tests + +Test files should be named using the following guidelines: + +### Documentation + +Documentation files should be named using the following guidelines: + +#### Roxygen Documentation + +Roxygen function documentation should be named using the following guidelines: + +## Function Naming Conventions + +### Function Names + +Function names should be named using the following guidelines: + +### Argument Names + +Function argument names should be named using the following guidelines: + +### Return Values + +Function return values should be named using the following guidelines: + +## Variable Naming Conventions + +### Variable Names + +Variable names should be named using the following guidelines: + +### Constants + +Constants should be named using the following guidelines: + +## Package Naming Conventions + +### Package Names + +Package names should be named using the following guidelines: + +### Namespace + +Namespace files should be named using the following guidelines: + +## Project Naming Conventions diff --git a/vignettes/noclocksR.Rmd b/vignettes/noclocksR.Rmd index d162bdb..70b8b06 100644 --- a/vignettes/noclocksR.Rmd +++ b/vignettes/noclocksR.Rmd @@ -1,8 +1,8 @@ --- -title: "noclocksR" +title: "noclocksr" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{noclocksR} + %\VignetteIndexEntry{noclocksr} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ``` diff --git a/vignettes/pkgdevt.Rmd b/vignettes/pkgdevt.Rmd index 2b808f8..890d883 100644 --- a/vignettes/pkgdevt.Rmd +++ b/vignettes/pkgdevt.Rmd @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ``` diff --git a/vignettes/plumber.Rmd b/vignettes/plumber.Rmd index b90eee2..6cbbdcd 100644 --- a/vignettes/plumber.Rmd +++ b/vignettes/plumber.Rmd @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ``` diff --git a/vignettes/shiny.Rmd b/vignettes/shiny.Rmd index 14a0806..8208a9b 100644 --- a/vignettes/shiny.Rmd +++ b/vignettes/shiny.Rmd @@ -15,7 +15,7 @@ knitr::opts_chunk$set( ``` ```{r setup} -# library(noclocksR) +# library(noclocksr) ``` ## Shiny Concepts diff --git a/vignettes/styleguide.Rmd b/vignettes/styleguide.Rmd index 07cfa16..aed189d 100644 --- a/vignettes/styleguide.Rmd +++ b/vignettes/styleguide.Rmd @@ -15,5 +15,5 @@ knitr::opts_chunk$set( ``` ```{r setup} -library(noclocksR) +library(noclocksr) ```