From 9e41c625a1c9a008685972c5b9ec781f9389781f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:32:28 +0100 Subject: [PATCH 01/12] AOP Fixes for v2 async (#3934) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../software/common/aop/AutoJobAspect.java | 89 ++----------------- .../software/common/model/job/JobResult.java | 3 + .../software/common/service/TaskManager.java | 6 +- .../SPDF/config/CleanUrlInterceptor.java | 3 +- 4 files changed, 14 insertions(+), 87 deletions(-) diff --git a/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java b/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java index 51c1882b6..9f01c4558 100644 --- a/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java +++ b/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java @@ -43,6 +43,7 @@ public class AutoJobAspect { // This aspect will run before any audit aspects due to @Order(0) // Extract parameters from the request and annotation boolean async = Boolean.parseBoolean(request.getParameter("async")); + log.debug("AutoJobAspect: Processing {} {} with async={}", request.getMethod(), request.getRequestURI(), async); long timeout = autoJobPostMapping.timeout(); int retryCount = autoJobPostMapping.retryCount(); boolean trackProgress = autoJobPostMapping.trackProgress(); @@ -54,19 +55,8 @@ public class AutoJobAspect { retryCount, trackProgress); - // Copy and process arguments - // In a test environment, we might need to update the original objects for verification - boolean isTestEnvironment = false; - try { - isTestEnvironment = Class.forName("org.junit.jupiter.api.Test") != null; - } catch (ClassNotFoundException e) { - // Not in a test environment - } - - Object[] args = - isTestEnvironment - ? processArgsInPlace(joinPoint.getArgs(), async) - : copyAndProcessArgs(joinPoint.getArgs(), async); + // Process arguments in-place to avoid type mismatch issues + Object[] args = processArgsInPlace(joinPoint.getArgs(), async); // Extract queueable and resourceWeight parameters and validate boolean queueable = autoJobPostMapping.queueable(); @@ -229,79 +219,10 @@ public class AutoJobAspect { resourceWeight); } - /** - * Creates deep copies of arguments when needed to avoid mutating the original objects - * Particularly important for PDFFile objects that might be reused by Spring - * - * @param originalArgs The original arguments - * @param async Whether this is an async operation - * @return A new array with safely processed arguments - */ - private Object[] copyAndProcessArgs(Object[] originalArgs, boolean async) { - if (originalArgs == null || originalArgs.length == 0) { - return originalArgs; - } - - Object[] processedArgs = new Object[originalArgs.length]; - - // Copy all arguments - for (int i = 0; i < originalArgs.length; i++) { - Object arg = originalArgs[i]; - - if (arg instanceof PDFFile pdfFile) { - // Create a copy of PDFFile to avoid mutating the original - // Using direct property access instead of reflection for better performance - PDFFile pdfFileCopy = new PDFFile(); - pdfFileCopy.setFileId(pdfFile.getFileId()); - pdfFileCopy.setFileInput(pdfFile.getFileInput()); - - // Case 1: fileId is provided but no fileInput - if (pdfFileCopy.getFileInput() == null && pdfFileCopy.getFileId() != null) { - try { - log.debug("Using fileId {} to get file content", pdfFileCopy.getFileId()); - MultipartFile file = fileStorage.retrieveFile(pdfFileCopy.getFileId()); - pdfFileCopy.setFileInput(file); - } catch (Exception e) { - throw new RuntimeException( - "Failed to resolve file by ID: " + pdfFileCopy.getFileId(), e); - } - } - // Case 2: For async requests, we need to make a copy of the MultipartFile - else if (async && pdfFileCopy.getFileInput() != null) { - try { - log.debug("Making persistent copy of uploaded file for async processing"); - MultipartFile originalFile = pdfFileCopy.getFileInput(); - String fileId = fileStorage.storeFile(originalFile); - - // Store the fileId for later reference - pdfFileCopy.setFileId(fileId); - - // Replace the original MultipartFile with our persistent copy - MultipartFile persistentFile = fileStorage.retrieveFile(fileId); - pdfFileCopy.setFileInput(persistentFile); - - log.debug("Created persistent file copy with fileId: {}", fileId); - } catch (IOException e) { - throw new RuntimeException( - "Failed to create persistent copy of uploaded file", e); - } - } - - processedArgs[i] = pdfFileCopy; - } else { - // For non-PDFFile objects, just pass the original reference - // If other classes need copy-on-write, add them here - processedArgs[i] = arg; - } - } - - return processedArgs; - } /** - * Processes arguments in-place for testing purposes This is similar to our original - * implementation before introducing copy-on-write It's only used in test environments to - * maintain test compatibility + * Processes arguments in-place to handle file resolution and async file persistence. + * This approach avoids type mismatch issues by modifying the original objects directly. * * @param originalArgs The original arguments * @param async Whether this is an async operation diff --git a/common/src/main/java/stirling/software/common/model/job/JobResult.java b/common/src/main/java/stirling/software/common/model/job/JobResult.java index 1aa66d1a8..e4eb456fd 100644 --- a/common/src/main/java/stirling/software/common/model/job/JobResult.java +++ b/common/src/main/java/stirling/software/common/model/job/JobResult.java @@ -6,6 +6,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import com.fasterxml.jackson.annotation.JsonIgnore; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,6 +30,7 @@ public class JobResult { private String error; /** List of result files for jobs that produce files */ + @JsonIgnore private List resultFiles; /** Time when the job was created */ diff --git a/common/src/main/java/stirling/software/common/service/TaskManager.java b/common/src/main/java/stirling/software/common/service/TaskManager.java index 219ae4ac4..902b2bfd1 100644 --- a/common/src/main/java/stirling/software/common/service/TaskManager.java +++ b/common/src/main/java/stirling/software/common/service/TaskManager.java @@ -1,6 +1,5 @@ package stirling.software.common.service; -import io.github.pixee.security.ZipSecurity; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -21,6 +20,8 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import io.github.pixee.security.ZipSecurity; + import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; @@ -361,7 +362,8 @@ public class TaskManager { MultipartFile zipFile = fileStorage.retrieveFile(zipFileId); try (ZipInputStream zipIn = - ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(zipFile.getBytes()))) { + ZipSecurity.createHardenedInputStream( + new ByteArrayInputStream(zipFile.getBytes()))) { ZipEntry entry; while ((entry = zipIn.getNextEntry()) != null) { if (!entry.isDirectory()) { diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index eb9f2be33..088c0c0bf 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -29,7 +29,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor { "type", "principal", "startDate", - "endDate"); + "endDate", + "async"); @Override public boolean preHandle( From 882170ebc9e448a825d33977167ee1e53d5050d2 Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 14 Jul 2025 12:57:46 +0200 Subject: [PATCH 02/12] ci: improve PR deployment workflow and labeling (#3842) # Description of Changes - Updated the labeler rules in `.github/labeler-config-srvaroa.yml` to support optional scope (e.g., `feat(api):`) for all conventional commit prefixes. - Added broader matching for API-related PRs by including `swagger` and `api` keywords in title matching. - Introduced a new `pr-deployed` label in `.github/labels.yml` to indicate that a PR has been deployed to a test environment. - Enhanced the `PR-Demo-Comment-with-react.yml` workflow: - Replaced `create-github-app-token` with a local `setup-bot` action to standardize GitHub App auth. - Added logic to automatically label deployed PRs with `pr-deployed`. - Added cleanup logic for temporary files after workflow execution. - Improved the `PR-Demo-cleanup.yml` workflow: - Triggered now on `pull_request_target` instead of `pull_request` for better permission context. - Automatically removes the `pr-deployed` label and any bot-generated deployment comment when a PR is closed. - Added proper GitHub App auth handling via `setup-bot`. - Ensured conditional cleanup only occurs if relevant artifacts are present. try: https://github.com/Stirling-Tools/Stirling-PDF/security/code-scanning/240 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --- .github/labeler-config-srvaroa.yml | 32 ++++--- .github/labels.yml | 3 + .../workflows/PR-Demo-Comment-with-react.yml | 72 ++++++++++------ .github/workflows/PR-Demo-cleanup.yml | 85 ++++++++++++++++++- 4 files changed, 154 insertions(+), 38 deletions(-) diff --git a/.github/labeler-config-srvaroa.yml b/.github/labeler-config-srvaroa.yml index b2324fbe3..e37a8a810 100644 --- a/.github/labeler-config-srvaroa.yml +++ b/.github/labeler-config-srvaroa.yml @@ -2,37 +2,46 @@ version: 1 labels: - label: "Bugfix" - title: '^fix:.*' + title: '^fix(\([^)]*\))?:|^fix:.*' - label: "enhancement" - title: '^feat:.*' + title: '^feat(\([^)]*\))?:|^feat:.*' - label: "build" - title: '^build:.*' + title: '^build(\([^)]*\))?:|^build:.*' - label: "chore" - title: '^chore:.*' + title: '^chore(\([^)]*\))?:|^chore:.*' - label: "ci" - title: '^ci:.*' + title: '^ci(\([^)]*\))?:|^ci:.*' + + - label: "ci" + title: '^.*\(ci\):.*' - label: "perf" - title: '^perf:.*' + title: '^perf(\([^)]*\))?:|^perf:.*' - label: "refactor" - title: '^refactor:.*' + title: '^refactor(\([^)]*\))?:|^refactor:.*' - label: "revert" - title: '^revert:.*' + title: '^revert(\([^)]*\))?:|^revert:.*' - label: "style" - title: '^style:.*' + title: '^style(\([^)]*\))?:|^style:.*' - label: "Documentation" - title: '^docs:.*' + title: '^docs(\([^)]*\))?:|^docs:.*' + + - label: "dependencies" + title: '^deps(\([^)]*\))?:|^deps:.*' + + - label: "dependencies" + title: '^.*\(deps\):.*' - label: 'API' - title: '.*openapi.*' + title: '.*openapi.*|.*swagger.*|.*api.*' - label: 'Translation' files: @@ -81,6 +90,7 @@ labels: - 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java' - 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/.*' - 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/.*' + - 'stirling-pdf/src/main/java/stirling/software/SPDF/service/ApiDocService.java' - 'proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*' - 'scripts/png_to_webp.py' - 'split_photos.py' diff --git a/.github/labels.yml b/.github/labels.yml index b7f5642e7..9b35ccb1a 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -175,3 +175,6 @@ description: "This PR changes 1000+ lines ignoring generated files." - name: "to research" color: "FBCA04" +- name: "pr-deployed" + color: "00FF00" + description: "Pull request has been deployed to a test environment" diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index edb696bf0..877a78524 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -6,20 +6,18 @@ on: permissions: contents: read - issues: write # Required for adding reactions to comments - pull-requests: read # Required for reading PR information + pull-requests: read jobs: check-comment: runs-on: ubuntu-latest permissions: issues: write - pull-requests: read if: | github.event.issue.pull_request && ( - contains(github.event.comment.body, 'prdeploy') || - contains(github.event.comment.body, 'deploypr') + contains(github.event.comment.body, 'prdeploy') || + contains(github.event.comment.body, 'deploypr') ) && ( @@ -47,10 +45,14 @@ jobs: with: egress-policy: audit - # Generate GitHub App token - - name: Generate GitHub App Token - id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup GitHub App Bot + if: github.actor != 'dependabot[bot]' + id: setup-bot + uses: ./.github/actions/setup-bot + continue-on-error: true with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -123,7 +125,7 @@ jobs: id: add-eyes-reaction uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: - github-token: ${{ steps.generate-token.outputs.token }} + github-token: ${{ steps.setup-bot.outputs.token }} script: | console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`); try { @@ -145,8 +147,8 @@ jobs: needs: check-comment runs-on: ubuntu-latest permissions: - contents: read issues: write + pull-requests: write steps: - name: Harden Runner @@ -154,9 +156,14 @@ jobs: with: egress-policy: audit - - name: Generate GitHub App Token - id: generate-token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup GitHub App Bot + if: github.actor != 'dependabot[bot]' + id: setup-bot + uses: ./.github/actions/setup-bot + continue-on-error: true with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -166,7 +173,7 @@ jobs: with: repository: ${{ needs.check-comment.outputs.pr_repository }} ref: ${{ needs.check-comment.outputs.pr_ref }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.setup-bot.outputs.token }} - name: Set up JDK uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 @@ -188,12 +195,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - - name: Get version number - id: versionNumber - run: | - VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') - echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - - name: Login to Docker Hub uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: @@ -297,7 +298,7 @@ jobs: if: success() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: - github-token: ${{ steps.generate-token.outputs.token }} + github-token: ${{ steps.setup-bot.outputs.token }} script: | console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -313,11 +314,26 @@ jobs: console.error(error); } + // add label to PR + const prNumber = ${{ needs.check-comment.outputs.pr_number }}; + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['pr-deployed'] + }); + console.log(`Added 'pr-deployed' label to PR #${prNumber}`); + } catch (error) { + console.error(`Failed to add label to PR: ${error.message}`); + console.error(error); + } + - name: Add failure reaction to comment if: failure() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: - github-token: ${{ steps.generate-token.outputs.token }} + github-token: ${{ steps.setup-bot.outputs.token }} script: | console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -337,7 +353,7 @@ jobs: if: success() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: - github-token: ${{ steps.generate-token.outputs.token }} + github-token: ${{ steps.setup-bot.outputs.token }} script: | const { GITHUB_REPOSITORY } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); @@ -357,3 +373,11 @@ jobs: issue_number: prNumber, body: commentBody }); + + - name: Cleanup temporary files + if: always() + run: | + echo "Cleaning up temporary files..." + rm -f ../private.key docker-compose.yml + echo "Cleanup complete." + continue-on-error: true diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index bec52c2bb..0cc6e3c1e 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -1,7 +1,7 @@ name: PR Deployment cleanup on: - pull_request: + pull_request_target: types: [opened, synchronize, reopened, closed] permissions: @@ -13,11 +13,11 @@ env: jobs: cleanup: + if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: - contents: write pull-requests: write - if: github.event.action == 'closed' + issues: write steps: - name: Harden Runner @@ -25,13 +25,84 @@ jobs: with: egress-policy: audit + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup GitHub App Bot + if: github.actor != 'dependabot[bot]' + id: setup-bot + uses: ./.github/actions/setup-bot + continue-on-error: true + with: + app-id: ${{ secrets.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Remove 'pr-deployed' label if present + id: remove-label-comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.setup-bot.outputs.token }} + script: | + const prNumber = ${{ github.event.pull_request.number }}; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Hole alle Labels auf dem PR + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: prNumber + }); + + const hasLabel = labels.some(label => label.name === 'pr-deployed'); + + if (hasLabel) { + console.log("Label 'pr-deployed' found. Removing..."); + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: prNumber, + name: 'pr-deployed' + }); + } else { + console.log("Label 'pr-deployed' not found. Nothing to do."); + } + + // Find existing comment + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber + }); + + const deploymentComments = comments.data.filter(c => + c.body?.includes("## 🚀 PR Test Deployment") && + c.user?.type === "Bot" + ); + + if (deploymentComments.length > 0) { + for (const comment of deploymentComments) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id + }); + console.log(`Deleted deployment comment (ID: ${comment.id})`); + } + } else { + console.log("No matching deployment comments found."); + } + core.setOutput('present', hasLabel || deploymentComment ? 'true' : 'false'); + - name: Set up SSH + if: steps.remove-label-comment.outputs.present == 'true' run: | mkdir -p ~/.ssh/ echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key sudo chmod 600 ../private.key - name: Cleanup PR deployment + if: steps.remove-label-comment.outputs.present == 'true' id: cleanup run: | ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH' @@ -57,3 +128,11 @@ jobs: echo "NO_CLEANUP_NEEDED" fi ENDSSH + + - name: Cleanup temporary files + if: always() + run: | + echo "Cleaning up temporary files..." + rm -f ../private.key + echo "Cleanup complete." + continue-on-error: true From b4df5c648a39687752ae8c6767c551fb15353c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:59:11 +0100 Subject: [PATCH 03/12] chore(deps): bump com.diffplug.spotless from 7.0.4 to 7.1.0 (#3904) Bumps com.diffplug.spotless from 7.0.4 to 7.1.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.diffplug.spotless&package-manager=gradle&previous-version=7.0.4&new-version=7.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a0d198c3a..113f690bf 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" id "edu.sc.seis.launch4j" version "3.0.6" - id "com.diffplug.spotless" version "7.0.4" + id "com.diffplug.spotless" version "7.1.0" id "com.github.jk1.dependency-license-report" version "2.9" //id "nebula.lint" version "19.0.3" id "org.panteleyev.jpackageplugin" version "1.7.3" From 17c75aee981f5a27b6f9c5ecd0bc69ab60cacb94 Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 14 Jul 2025 13:01:22 +0200 Subject: [PATCH 04/12] chore(license-report): add `projects = [project]` to licenseReport to avoid deprecation warnings (#3933) # Description of Changes - **What was changed** Added the line `projects = [project]` to the `licenseReport` configuration in `build.gradle`. - **Why the change was made** Without specifying `projects`, the `licenseReport` plugin attempts to resolve configurations from a non-project context, resulting in numerous deprecation warnings. Explicitly setting `projects = [project]` scopes the report to the current project and silences these warnings. ``` - [warn] Resolution of the configuration :common:runtimeClasspath was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration146 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration147 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration148 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration149 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration150 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration151 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:detachedConfiguration152 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:developmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :common:testAndDevelopmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:runtimeClasspath was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration215 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration216 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration217 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration218 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration219 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:detachedConfiguration220 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:developmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :proprietary:testAndDevelopmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :stirling-pdf:runtimeClasspath was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :stirling-pdf:detachedConfiguration231 was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :stirling-pdf:developmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. - [warn] Resolution of the configuration :stirling-pdf:testAndDevelopmentOnly was attempted from a context different than the project context. Have a look at the documentation to understand why this is a problem and how it can be resolved. This behavior has been deprecated. ``` --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 113f690bf..54d702673 100644 --- a/build.gradle +++ b/build.gradle @@ -169,6 +169,7 @@ tasks.withType(JavaCompile).configureEach { } licenseReport { + projects = [project] renderers = [new JsonReportRenderer()] allowedLicensesFile = new File("$projectDir/allowed-licenses.json") } From 4ad293dd3b949295caec7957bec2f082e16ce21d Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 14 Jul 2025 13:02:13 +0200 Subject: [PATCH 05/12] ci: fix Swagger docs generation by targeting stirling-pdf module (#3935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes **What was changed** - Updated the GitHub Actions workflow (`.github/workflows/swagger.yml`) to invoke the `:stirling-pdf:generateOpenApiDocs` task instead of the root `generateOpenApiDocs`. Refactored `build.gradle` to apply the `org.springdoc.openapi-gradle-plugin` exclusively to the `stirling-pdf` subproject, configured its `openApi` extension, and introduced new Gradle tasks—`copySwaggerDoc` and `cleanSwaggerInBuild`—to manage the generated `SwaggerDoc.json` file correctly. **Why the change was made** - The previous configuration failed to generate OpenAPI documentation for the `stirling-pdf` module. These changes ensure that Swagger documentation is produced from the correct module, uploaded to SwaggerHub as intended, and that temporary artifacts are cleaned up to maintain a tidy build directory. try #3932 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/workflows/swagger.yml | 2 +- build.gradle | 48 +++++++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index d717d5563..463736b65 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -29,7 +29,7 @@ jobs: - uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 - name: Generate Swagger documentation - run: ./gradlew generateOpenApiDocs + run: ./gradlew :stirling-pdf:generateOpenApiDocs - name: Upload Swagger Documentation to SwaggerHub run: ./gradlew swaggerhubUpload diff --git a/build.gradle b/build.gradle index 54d702673..84f2c1cb3 100644 --- a/build.gradle +++ b/build.gradle @@ -161,6 +161,44 @@ subprojects { tasks.named("processResources") { dependsOn(rootProject.tasks.writeVersion) } + + if (name == 'stirling-pdf') { + apply plugin: 'org.springdoc.openapi-gradle-plugin' + + openApi { + apiDocsUrl = "http://localhost:8080/v1/api-docs" + outputDir = file("$projectDir") + outputFileName = "SwaggerDoc.json" + waitTimeInSeconds = 60 // Increase the wait time to 60 seconds + } + + tasks.named("forkedSpringBootRun") { + dependsOn(":common:jar") + dependsOn(":proprietary:jar") + } + + tasks.register("copySwaggerDoc", Copy) { + doNotTrackState("Writes SwaggerDoc.json to project root") + from(layout.projectDirectory.file("SwaggerDoc.json")) + into(rootProject.projectDir) + dependsOn("generateOpenApiDocs") + } + + tasks.register("cleanSwaggerInBuild", Delete) { + doNotTrackState("Cleans up SwaggerDoc.json in build directory") + delete(layout.projectDirectory.file("SwaggerDoc.json")) + dependsOn("copySwaggerDoc") + } + + tasks.named("copySwaggerDoc") { + finalizedBy("cleanSwaggerInBuild") + } + + tasks.named("generateOpenApiDocs") { + finalizedBy("copySwaggerDoc") + doNotTrackState("OpenAPI plugin writes outside build directory") + } + } } tasks.withType(JavaCompile).configureEach { @@ -205,13 +243,6 @@ sourceSets { } } -openApi { - apiDocsUrl = "http://localhost:8080/v1/api-docs" - outputDir = file("$projectDir") - outputFileName = "SwaggerDoc.json" - waitTimeInSeconds = 60 // Increase the wait time to 60 seconds -} - // Configure the forked spring boot run task to properly delegate to the stirling-pdf module tasks.named('forkedSpringBootRun') { dependsOn ':stirling-pdf:bootRun' @@ -566,9 +597,6 @@ tasks.register('printMacVersion') { } } -tasks.named('generateOpenApiDocs') { - doNotTrackState("Tracking state is not supported for this task") -} tasks.named('bootRun') { group = 'application' description = 'Delegates to :stirling-pdf:bootRun' From b2f1404f68708d8f0b1400ed1503d860689d198e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:03:28 +0100 Subject: [PATCH 06/12] chore(deps): bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 (#3939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.commons:commons-lang3&package-manager=gradle&previous-version=3.17.0&new-version=3.18.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- common/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/build.gradle b/common/build.gradle index 6dfd222bf..2ab8c3b97 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -21,7 +21,7 @@ dependencies { api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' - api 'org.apache.commons:commons-lang3:3.17.0' + api 'org.apache.commons:commons-lang3:3.18.0' api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' api "org.apache.pdfbox:pdfbox:$pdfboxVersion" From 03f184ab2bb2cfdb470de4b4f6ca60d324ca9fce Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 14 Jul 2025 13:05:17 +0200 Subject: [PATCH 07/12] chore(cucumber): add create_pdf_with_black_boxes and convert-pdf-to-image outline; remove duplicate split-pdf-by-sections (#3937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What was changed** - Introduced `create_pdf_with_black_boxes` helper function in `environment.py` for generating test PDFs with occluded content. - Added **Scenario Outline: Convert PDF to image** to `conversion.feature` to validate PDF→image conversion workflows. - Removed the duplicate **Scenario Outline: split-pdf-by-sections with different parameters** from `general.feature`. - **Why the change was made** - To enable testing of blacked-out content scenarios and ensure our suite covers image conversion. - To eliminate redundant tests and keep the feature files DRY and maintainable. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- testing/allEndpointsRemovedSettings.yml | 30 +- testing/cucumber/features/environment.py | 16 +- testing/cucumber/features/examples.feature | 238 ++++----- testing/cucumber/features/external.feature | 456 +++++++++--------- testing/cucumber/features/general.feature | 176 +++---- .../features/steps/step_definitions.py | 281 +++++++---- 6 files changed, 664 insertions(+), 533 deletions(-) diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index 8230b4418..7240fe128 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -65,17 +65,23 @@ premium: key: 00000000-0000-0000-0000-000000000000 enabled: false # Enable license key checks for pro/enterprise features proFeatures: + database: true # Enable database features SSOAutoLogin: false CustomMetadata: - autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values - author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username - creator: Stirling-PDF # supports text such as 'Company-PDF' - producer: Stirling-PDF # supports text such as 'Company-PDF' + autoUpdateMetadata: false + author: username + creator: Stirling-PDF + producer: Stirling-PDF googleDrive: enabled: false clientId: '' apiKey: '' appId: '' + enterpriseFeatures: + audit: + enabled: true # Enable audit logging + level: 2 # Audit logging level: 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE + retentionDays: 90 # Number of days to retain audit logs mail: enabled: false # set to 'true' to enable sending emails @@ -86,7 +92,7 @@ mail: from: '' # sender email address legal: - termsAndConditions: https://www.stirlingpdf.com/terms # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder + termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder @@ -120,6 +126,15 @@ system: weasyprint: '' # Defaults to /opt/venv/bin/weasyprint unoconvert: '' # Defaults to /opt/venv/bin/unoconvert fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB". + tempFileManagement: + baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf + libreofficeDir: '' # Defaults to tempFileManagement.baseTmpDir/libreoffice + systemTempDir: '' # Only used if cleanupSystemTemp is true + prefix: stirling-pdf- # Prefix for temp file names + maxAgeHours: 24 # Maximum age in hours before temp files are cleaned up + cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes) + startupCleanup: true # Clean up old temp files on startup + cleanupSystemTemp: false # Whether to clean broader system temp directory ui: appName: '' # application's visible name @@ -150,6 +165,8 @@ processExecutor: weasyPrintSessionLimit: 16 installAppSessionLimit: 1 calibreSessionLimit: 1 + ghostscriptSessionLimit: 8 + ocrMyPdfSessionLimit: 2 timeoutMinutes: # Process executor timeout in minutes libreOfficetimeoutMinutes: 30 pdfToHtmltimeoutMinutes: 20 @@ -158,3 +175,6 @@ processExecutor: installApptimeoutMinutes: 60 calibretimeoutMinutes: 30 tesseractTimeoutMinutes: 30 + qpdfTimeoutMinutes: 30 + ghostscriptTimeoutMinutes: 30 + ocrMyPdfTimeoutMinutes: 30 diff --git a/testing/cucumber/features/environment.py b/testing/cucumber/features/environment.py index c85eb001d..ac6676f86 100644 --- a/testing/cucumber/features/environment.py +++ b/testing/cucumber/features/environment.py @@ -1,21 +1,25 @@ import os + def before_all(context): context.endpoint = None context.request_data = None context.files = {} context.response = None + def after_scenario(context, scenario): - if hasattr(context, 'files'): + if hasattr(context, "files"): for file in context.files.values(): file.close() - if os.path.exists('response_file'): - os.remove('response_file') - if hasattr(context, 'file_name') and os.path.exists(context.file_name): + if os.path.exists("response_file"): + os.remove("response_file") + if hasattr(context, "file_name") and os.path.exists(context.file_name): os.remove(context.file_name) # Remove any temporary files - for temp_file in os.listdir('.'): - if temp_file.startswith('genericNonCustomisableName') or temp_file.startswith('temp_image_'): + for temp_file in os.listdir("."): + if temp_file.startswith("genericNonCustomisableName") or temp_file.startswith( + "temp_image_" + ): os.remove(temp_file) diff --git a/testing/cucumber/features/examples.feature b/testing/cucumber/features/examples.feature index 3594861d2..398a80ce1 100644 --- a/testing/cucumber/features/examples.feature +++ b/testing/cucumber/features/examples.feature @@ -1,132 +1,132 @@ @example @general Feature: API Validation - @positive @password - Scenario: Remove password - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages - And the pdf is encrypted with password "password123" - And the request data includes - | parameter | value | - | password | password123 | - When I send the API request to the endpoint "/api/v1/security/remove-password" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response PDF is not passworded - And the response status code should be 200 + @positive @password + Scenario: Remove password + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages + And the pdf is encrypted with password "password123" + And the request data includes + | parameter | value | + | password | password123 | + When I send the API request to the endpoint "/api/v1/security/remove-password" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response PDF is not passworded + And the response status code should be 200 - @negative @password - Scenario: Remove password wrong password - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages - And the pdf is encrypted with password "password123" - And the request data includes - | parameter | value | - | password | wrongPassword | - When I send the API request to the endpoint "/api/v1/security/remove-password" - Then the response status code should be 500 - And the response should contain error message "Internal Server Error" + @negative @password + Scenario: Remove password wrong password + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages + And the pdf is encrypted with password "password123" + And the request data includes + | parameter | value | + | password | wrongPassword | + When I send the API request to the endpoint "/api/v1/security/remove-password" + Then the response status code should be 500 + And the response should contain error message "Internal Server Error" - @positive @info - Scenario: Get info - Given I generate a PDF file as "fileInput" - When I send the API request to the endpoint "/api/v1/security/get-info-on-pdf" - Then the response content type should be "application/json" - And the response file should have size greater than 100 - And the response status code should be 200 + @positive @info + Scenario: Get info + Given I generate a PDF file as "fileInput" + When I send the API request to the endpoint "/api/v1/security/get-info-on-pdf" + Then the response content type should be "application/json" + And the response file should have size greater than 100 + And the response status code should be 200 - @positive @password - Scenario: Add password - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages - And the request data includes - | parameter | value | - | password | password123 | - When I send the API request to the endpoint "/api/v1/security/add-password" - Then the response content type should be "application/pdf" - And the response file should have size greater than 100 - And the response PDF is passworded - And the response status code should be 200 + @positive @password + Scenario: Add password + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages + And the request data includes + | parameter | value | + | password | password123 | + When I send the API request to the endpoint "/api/v1/security/add-password" + Then the response content type should be "application/pdf" + And the response file should have size greater than 100 + And the response PDF is passworded + And the response status code should be 200 - @positive @password - Scenario: Add password with other params - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages - And the request data includes - | parameter | value | - | ownerPassword | ownerPass | - | password | password123 | - | keyLength | 256 | - | canPrint | true | - | canModify | false | - When I send the API request to the endpoint "/api/v1/security/add-password" - Then the response content type should be "application/pdf" - And the response file should have size greater than 100 - And the response PDF is passworded - And the response status code should be 200 + @positive @password + Scenario: Add password with other params + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages + And the request data includes + | parameter | value | + | ownerPassword | ownerPass | + | password | password123 | + | keyLength | 256 | + | canPrint | true | + | canModify | false | + When I send the API request to the endpoint "/api/v1/security/add-password" + Then the response content type should be "application/pdf" + And the response file should have size greater than 100 + And the response PDF is passworded + And the response status code should be 200 - @positive @watermark - Scenario: Add watermark - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages - And the request data includes - | parameter | value | - | watermarkType | text | - | watermarkText | Sample Watermark | - | fontSize | 30 | - | rotation | 45 | - | opacity | 0.5 | - | widthSpacer | 50 | - | heightSpacer | 50 | - | alphabet | roman | - | customColor | #d3d3d3 | - When I send the API request to the endpoint "/api/v1/security/add-watermark" - Then the response content type should be "application/pdf" - And the response file should have size greater than 100 - And the response status code should be 200 + @positive @watermark + Scenario: Add watermark + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages + And the request data includes + | parameter | value | + | watermarkType | text | + | watermarkText | Sample Watermark | + | fontSize | 30 | + | rotation | 45 | + | opacity | 0.5 | + | widthSpacer | 50 | + | heightSpacer | 50 | + | alphabet | roman | + | customColor | #d3d3d3 | + When I send the API request to the endpoint "/api/v1/security/add-watermark" + Then the response content type should be "application/pdf" + And the response file should have size greater than 100 + And the response status code should be 200 - @positive - Scenario: Remove blank pages - Given I generate a PDF file as "fileInput" - And the pdf contains 3 blank pages - And the request data includes - | parameter | value | - | threshold | 90 | - | whitePercent | 99.9 | - When I send the API request to the endpoint "/api/v1/misc/remove-blanks" - Then the response content type should be "application/octet-stream" - And the response file should have extension ".zip" - And the response ZIP should contain 1 files - And the response file should have size greater than 0 + @positive + Scenario: Remove blank pages + Given I generate a PDF file as "fileInput" + And the pdf contains 3 blank pages + And the request data includes + | parameter | value | + | threshold | 90 | + | whitePercent | 99.9 | + When I send the API request to the endpoint "/api/v1/misc/remove-blanks" + Then the response content type should be "application/octet-stream" + And the response file should have extension ".zip" + And the response ZIP should contain 1 files + And the response file should have size greater than 0 - @positive @flatten - Scenario: Flatten PDF - Given I generate a PDF file as "fileInput" - And the request data includes - | parameter | value | - | flattenOnlyForms | false | - When I send the API request to the endpoint "/api/v1/misc/flatten" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response status code should be 200 + @positive @flatten + Scenario: Flatten PDF + Given I generate a PDF file as "fileInput" + And the request data includes + | parameter | value | + | flattenOnlyForms | false | + When I send the API request to the endpoint "/api/v1/misc/flatten" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response status code should be 200 - @positive @metadata - Scenario: Update metadata - Given I generate a PDF file as "fileInput" - And the request data includes - | parameter | value | - | author | John Doe | - | title | Sample Title | - | subject | Sample Subject | - | keywords | sample, test | - | producer | Test Producer | - When I send the API request to the endpoint "/api/v1/misc/update-metadata" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response PDF metadata should include "Author" as "John Doe" - And the response PDF metadata should include "Keywords" as "sample, test" - And the response PDF metadata should include "Subject" as "Sample Subject" - And the response PDF metadata should include "Title" as "Sample Title" - And the response status code should be 200 + @positive @metadata + Scenario: Update metadata + Given I generate a PDF file as "fileInput" + And the request data includes + | parameter | value | + | author | John Doe | + | title | Sample Title | + | subject | Sample Subject | + | keywords | sample, test | + | producer | Test Producer | + When I send the API request to the endpoint "/api/v1/misc/update-metadata" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response PDF metadata should include "Author" as "John Doe" + And the response PDF metadata should include "Keywords" as "sample, test" + And the response PDF metadata should include "Subject" as "Sample Subject" + And the response PDF metadata should include "Title" as "Sample Title" + And the response status code should be 200 diff --git a/testing/cucumber/features/external.feature b/testing/cucumber/features/external.feature index 23ef039e7..ad83a01ae 100644 --- a/testing/cucumber/features/external.feature +++ b/testing/cucumber/features/external.feature @@ -1,230 +1,250 @@ Feature: API Validation - @libre @positive - Scenario: Repair PDF - Given I generate a PDF file as "fileInput" - When I send the API request to the endpoint "/api/v1/misc/repair" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response status code should be 200 - - - @ocr @positive - Scenario: Process PDF with OCR - Given I generate a PDF file as "fileInput" - And the request data includes - | parameter | value | - | languages | eng | - | sidecar | false | - | deskew | true | - | clean | true | - | cleanFinal | true | - | ocrType | Normal | - | ocrRenderType | hocr | - | removeImagesAfter| false | - When I send the API request to the endpoint "/api/v1/misc/ocr-pdf" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response status code should be 200 + @libre @positive + Scenario: Repair PDF + Given I generate a PDF file as "fileInput" + When I send the API request to the endpoint "/api/v1/misc/repair" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response status code should be 200 - @ocr @positive - Scenario: Extract Image Scans - Given I generate a PDF file as "fileInput" - And the pdf contains 3 images of size 300x300 on 2 pages - And the request data includes - | parameter | value | - | angleThreshold | 5 | - | tolerance | 20 | - | minArea | 8000 | - | minContourArea | 500 | - | borderSize | 1 | - When I send the API request to the endpoint "/api/v1/misc/extract-image-scans" - Then the response content type should be "application/octet-stream" - And the response file should have extension ".zip" - And the response ZIP should contain 2 files - And the response file should have size greater than 0 - And the response status code should be 200 - - - @ocr @positive - Scenario: Process PDF with OCR - Given I generate a PDF file as "fileInput" - And the request data includes - | parameter | value | - | languages | eng | - | sidecar | false | - | deskew | true | - | clean | true | - | cleanFinal | true | - | ocrType | Force | - | ocrRenderType | hocr | - | removeImagesAfter| false | - When I send the API request to the endpoint "/api/v1/misc/ocr-pdf" - Then the response content type should be "application/pdf" - And the response file should have size greater than 0 - And the response status code should be 200 + @ocr @positive + Scenario: Process PDF with OCR + Given I generate a PDF file as "fileInput" + And the request data includes + | parameter | value | + | languages | eng | + | sidecar | false | + | deskew | true | + | clean | true | + | cleanFinal | true | + | ocrType | Normal | + | ocrRenderType | hocr | + | removeImagesAfter | false | + When I send the API request to the endpoint "/api/v1/misc/ocr-pdf" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response status code should be 200 - @libre @positive - Scenario Outline: Convert PDF to various word formats - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages with random text - And the request data includes - | parameter | value | - | outputFormat | | - When I send the API request to the endpoint "/api/v1/convert/pdf/word" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension "" + @ocr @positive + Scenario: Extract Image Scans + Given I generate a PDF file as "fileInput" + And the pdf contains 3 images of size 300x300 on 2 pages + And the request data includes + | parameter | value | + | angleThreshold | 5 | + | tolerance | 20 | + | minArea | 8000 | + | minContourArea | 500 | + | borderSize | 1 | + When I send the API request to the endpoint "/api/v1/misc/extract-image-scans" + Then the response content type should be "application/octet-stream" + And the response file should have extension ".zip" + And the response ZIP should contain 2 files + And the response file should have size greater than 0 + And the response status code should be 200 - Examples: - | format | extension | - | docx | .docx | - | odt | .odt | - | doc | .doc | - @ocr @pdfa1 - Scenario: PDFA - Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | outputFormat | pdfa | - When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa" - Then the response status code should be 200 - And the response file should have extension ".pdf" - And the response file should have size greater than 100 - - @ocr @pdfa2 - Scenario: PDFA1 - Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | outputFormat | pdfa-1 | - When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa" - Then the response status code should be 200 - And the response file should have extension ".pdf" - And the response file should have size greater than 100 - - @compress @qpdf @positive - Scenario: Compress - Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | optimizeLevel | 4 | - When I send the API request to the endpoint "/api/v1/misc/compress-pdf" - Then the response status code should be 200 - And the response file should have extension ".pdf" - And the response file should have size greater than 100 - - @compress @qpdf @positive - Scenario: Compress - Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | optimizeLevel | 1 | - | expectedOutputSize | 5KB | - When I send the API request to the endpoint "/api/v1/misc/compress-pdf" - Then the response status code should be 200 - And the response file should have extension ".pdf" - And the response file should have size greater than 100 - - - @compress @qpdf @positive - Scenario: Compress - Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | optimizeLevel | 1 | - | expectedOutputSize | 5KB | - When I send the API request to the endpoint "/api/v1/misc/compress-pdf" - Then the response status code should be 200 - And the response file should have extension ".pdf" - And the response file should have size greater than 100 - - @libre @positive - Scenario Outline: Convert PDF to various types - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages with random text - And the request data includes - | parameter | value | - | outputFormat | | - When I send the API request to the endpoint "/api/v1/convert/pdf/" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension "" + @ocr @positive + Scenario: Process PDF with OCR + Given I generate a PDF file as "fileInput" + And the request data includes + | parameter | value | + | languages | eng | + | sidecar | false | + | deskew | true | + | clean | true | + | cleanFinal | true | + | ocrType | Force | + | ocrRenderType | hocr | + | removeImagesAfter | false | + When I send the API request to the endpoint "/api/v1/misc/ocr-pdf" + Then the response content type should be "application/pdf" + And the response file should have size greater than 0 + And the response status code should be 200 - Examples: - | type | format | extension | - | text | rtf | .rtf | - | text | txt | .txt | - | presentation | ppt | .ppt | - | presentation | pptx | .pptx | - | presentation | odp | .odp | - | html | html | .zip | - - @libre @positive @topdf - Scenario Outline: Convert PDF to various types - Given I use an example file at "exampleFiles/example" as parameter "fileInput" - When I send the API request to the endpoint "/api/v1/convert/file/pdf" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension ".pdf" + @libre @positive + Scenario Outline: Convert PDF to various word formats + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages with random text + And the request data includes + | parameter | value | + | outputFormat | | + When I send the API request to the endpoint "/api/v1/convert/pdf/word" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension "" - Examples: - | extension | - | .docx | - | .odp | - | .odt | - | .pptx | - | .rtf | - - @calibre @positive @htmltopdf - Scenario: Convert HTML to PDF - Given I use an example file at "exampleFiles/example.html" as parameter "fileInput" - When I send the API request to the endpoint "/api/v1/convert/html/pdf" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension ".pdf" - - @calibre @positive @zippedhtmltopdf - Scenario: Convert zipped HTML to PDF - Given I use an example file at "exampleFiles/example_html.zip" as parameter "fileInput" - When I send the API request to the endpoint "/api/v1/convert/html/pdf" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension ".pdf" - - @calibre @positive @markdowntopdf - Scenario: Convert Markdown to PDF - Given I use an example file at "exampleFiles/example.md" as parameter "fileInput" - When I send the API request to the endpoint "/api/v1/convert/markdown/pdf" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension ".pdf" - - @markdown @positive - Scenario: Convert PDF to Markdown format - Given I generate a PDF file as "fileInput" - And the pdf contains 3 pages with random text - When I send the API request to the endpoint "/api/v1/convert/pdf/markdown" - Then the response status code should be 200 - And the response file should have size greater than 100 - And the response file should have extension ".md" - - - @positive @pdftocsv - Scenario: Convert PDF with tables to CSV format - Given I use an example file at "exampleFiles/tables.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | outputFormat | csv | - | pageNumbers | all | - When I send the API request to the endpoint "/api/v1/convert/pdf/csv" - Then the response status code should be 200 - And the response file should have size greater than 200 - And the response file should have extension ".zip" - And the response ZIP should contain 3 files - \ No newline at end of file + Examples: + | format | extension | + | docx | .docx | + | odt | .odt | + | doc | .doc | + + @ocr @pdfa1 + Scenario: PDFA + Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | outputFormat | pdfa | + When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa" + Then the response status code should be 200 + And the response file should have extension ".pdf" + And the response file should have size greater than 100 + + @ocr @pdfa2 + Scenario: PDFA1 + Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | outputFormat | pdfa-1 | + When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa" + Then the response status code should be 200 + And the response file should have extension ".pdf" + And the response file should have size greater than 100 + + @compress @qpdf @positive + Scenario: Compress + Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | optimizeLevel | 4 | + When I send the API request to the endpoint "/api/v1/misc/compress-pdf" + Then the response status code should be 200 + And the response file should have extension ".pdf" + And the response file should have size greater than 100 + + @compress @qpdf @positive + Scenario: Compress + Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | optimizeLevel | 1 | + | expectedOutputSize | 5KB | + When I send the API request to the endpoint "/api/v1/misc/compress-pdf" + Then the response status code should be 200 + And the response file should have extension ".pdf" + And the response file should have size greater than 100 + + + @compress @qpdf @positive + Scenario: Compress + Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | optimizeLevel | 1 | + | expectedOutputSize | 5KB | + When I send the API request to the endpoint "/api/v1/misc/compress-pdf" + Then the response status code should be 200 + And the response file should have extension ".pdf" + And the response file should have size greater than 100 + + @libre @positive + Scenario Outline: Convert PDF to various types + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages with random text + And the request data includes + | parameter | value | + | outputFormat | | + When I send the API request to the endpoint "/api/v1/convert/pdf/" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension "" + + Examples: + | type | format | extension | + | text | rtf | .rtf | + | text | txt | .txt | + | presentation | ppt | .ppt | + | presentation | pptx | .pptx | + | presentation | odp | .odp | + | html | html | .zip | + + @image @positive + Scenario Outline: Convert PDF to image + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages with random text + And the pdf contains 3 images of size 300x300 on 3 pages + And the request data includes + | parameter | value | + | dpi | 300 | + | imageFormat | | + When I send the API request to the endpoint "/api/v1/convert/pdf/img" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".zip" + + Examples: + | format | + | webp | + | png | + | jpeg | + | jpg | + | gif | + + @libre @positive @topdf + Scenario Outline: Convert PDF to various types + Given I use an example file at "exampleFiles/example" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/file/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + Examples: + | extension | + | .docx | + | .odp | + | .odt | + | .pptx | + | .rtf | + + @calibre @positive @htmltopdf + Scenario: Convert HTML to PDF + Given I use an example file at "exampleFiles/example.html" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/html/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + @calibre @positive @zippedhtmltopdf + Scenario: Convert zipped HTML to PDF + Given I use an example file at "exampleFiles/example_html.zip" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/html/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + @calibre @positive @markdowntopdf + Scenario: Convert Markdown to PDF + Given I use an example file at "exampleFiles/example.md" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/markdown/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + @markdown @positive + Scenario: Convert PDF to Markdown format + Given I generate a PDF file as "fileInput" + And the pdf contains 3 pages with random text + When I send the API request to the endpoint "/api/v1/convert/pdf/markdown" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".md" + + + @positive @pdftocsv + Scenario: Convert PDF with tables to CSV format + Given I use an example file at "exampleFiles/tables.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | outputFormat | csv | + | pageNumbers | all | + When I send the API request to the endpoint "/api/v1/convert/pdf/csv" + Then the response status code should be 200 + And the response file should have size greater than 200 + And the response file should have extension ".zip" + And the response ZIP should contain 3 files diff --git a/testing/cucumber/features/general.feature b/testing/cucumber/features/general.feature index 3ac610669..9736e2f30 100644 --- a/testing/cucumber/features/general.feature +++ b/testing/cucumber/features/general.feature @@ -2,113 +2,89 @@ Feature: API Validation - @split-pdf-by-sections @positive - Scenario Outline: split-pdf-by-sections with different parameters - Given I generate a PDF file as "fileInput" - And the pdf contains 2 pages - And the request data includes - | parameter | value | - | horizontalDivisions | | - | verticalDivisions | | - | merge | true | - When I send the API request to the endpoint "/api/v1/general/split-pdf-by-sections" - Then the response content type should be "application/pdf" - And the response file should have size greater than 200 - And the response status code should be 200 - And the response PDF should contain pages + @split-pdf-by-sections @positive + Scenario Outline: split-pdf-by-sections with different parameters + Given I generate a PDF file as "fileInput" + And the pdf contains 2 pages + And the request data includes + | parameter | value | + | horizontalDivisions | | + | verticalDivisions | | + | merge | true | + When I send the API request to the endpoint "/api/v1/general/split-pdf-by-sections" + Then the response content type should be "application/pdf" + And the response file should have size greater than 200 + And the response status code should be 200 + And the response PDF should contain pages - Examples: - | horizontalDivisions | verticalDivisions | page_count | - | 0 | 1 | 4 | - | 1 | 1 | 8 | - | 1 | 2 | 12 | - | 2 | 2 | 18 | - - @split-pdf-by-sections @positive - Scenario Outline: split-pdf-by-sections with different parameters - Given I generate a PDF file as "fileInput" - And the pdf contains 2 pages - And the request data includes - | parameter | value | - | horizontalDivisions | | - | verticalDivisions | | - | merge | true | - When I send the API request to the endpoint "/api/v1/general/split-pdf-by-sections" - Then the response content type should be "application/pdf" - And the response file should have size greater than 200 - And the response status code should be 200 - And the response PDF should contain pages - - Examples: - | horizontalDivisions | verticalDivisions | page_count | - | 0 | 1 | 4 | - | 1 | 1 | 8 | - | 1 | 2 | 12 | - | 2 | 2 | 18 | + Examples: + | horizontalDivisions | verticalDivisions | page_count | + | 0 | 1 | 4 | + | 1 | 1 | 8 | + | 1 | 2 | 12 | + | 2 | 2 | 18 | + @split-pdf-by-pages @positive + Scenario Outline: split-pdf-by-pages with different parameters + Given I generate a PDF file as "fileInput" + And the pdf contains 20 pages + And the request data includes + | parameter | value | + | fileInput | fileInput | + | pageNumbers | | + When I send the API request to the endpoint "/api/v1/general/split-pages" + Then the response content type should be "application/octet-stream" + And the response status code should be 200 + And the response file should have size greater than 200 + And the response ZIP should contain files - @split-pdf-by-pages @positive - Scenario Outline: split-pdf-by-pages with different parameters - Given I generate a PDF file as "fileInput" - And the pdf contains 20 pages - And the request data includes - | parameter | value | - | fileInput | fileInput | - | pageNumbers | | - When I send the API request to the endpoint "/api/v1/general/split-pages" - Then the response content type should be "application/octet-stream" - And the response status code should be 200 - And the response file should have size greater than 200 - And the response ZIP should contain files - - Examples: - | pageNumbers | file_count | - | 1,3,5-9 | 8 | - | all | 20 | - | 2n+1 | 10 | - | 3n | 7 | + Examples: + | pageNumbers | file_count | + | 1,3,5-9 | 8 | + | all | 20 | + | 2n+1 | 10 | + | 3n | 7 | + @split-pdf-by-size-or-count @positive + Scenario Outline: split-pdf-by-size-or-count with different parameters + Given I generate a PDF file as "fileInput" + And the pdf contains 20 pages + And the request data includes + | parameter | value | + | fileInput | fileInput | + | splitType | | + | splitValue | | + When I send the API request to the endpoint "/api/v1/general/split-by-size-or-count" + Then the response content type should be "application/octet-stream" + And the response status code should be 200 + And the response file should have size greater than 200 + And the response ZIP file should contain documents each having pages - @split-pdf-by-size-or-count @positive - Scenario Outline: split-pdf-by-size-or-count with different parameters - Given I generate a PDF file as "fileInput" - And the pdf contains 20 pages - And the request data includes - | parameter | value | - | fileInput | fileInput | - | splitType | | - | splitValue | | - When I send the API request to the endpoint "/api/v1/general/split-by-size-or-count" - Then the response content type should be "application/octet-stream" - And the response status code should be 200 - And the response file should have size greater than 200 - And the response ZIP file should contain documents each having pages - - Examples: - | splitType | splitValue | doc_count | pages_per_doc | - | 1 | 5 | 4 | 5 | - | 2 | 2 | 2 | 10 | - | 2 | 4 | 4 | 5 | - | 1 | 10 | 2 | 10 | + Examples: + | splitType | splitValue | doc_count | pages_per_doc | + | 1 | 5 | 4 | 5 | + | 2 | 2 | 2 | 10 | + | 2 | 4 | 4 | 5 | + | 1 | 10 | 2 | 10 | - @extract-images - Scenario Outline: Extract Image Scans duplicates - Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput" - And the request data includes - | parameter | value | - | format | | - When I send the API request to the endpoint "/api/v1/misc/extract-images" - Then the response content type should be "application/octet-stream" - And the response file should have extension ".zip" - And the response ZIP should contain 2 files - And the response file should have size greater than 0 - And the response status code should be 200 + @extract-images + Scenario Outline: Extract Image Scans duplicates + Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput" + And the request data includes + | parameter | value | + | format | | + When I send the API request to the endpoint "/api/v1/misc/extract-images" + Then the response content type should be "application/octet-stream" + And the response file should have extension ".zip" + And the response ZIP should contain 2 files + And the response file should have size greater than 0 + And the response status code should be 200 - Examples: - | format | - | png | - | gif | - | jpeg | + Examples: + | format | + | png | + | gif | + | jpeg | diff --git a/testing/cucumber/features/steps/step_definitions.py b/testing/cucumber/features/steps/step_definitions.py index 5f2a92eca..7c3b996b7 100644 --- a/testing/cucumber/features/steps/step_definitions.py +++ b/testing/cucumber/features/steps/step_definitions.py @@ -10,67 +10,67 @@ from reportlab.lib.pagesizes import letter from reportlab.lib.utils import ImageReader from reportlab.pdfgen import canvas import mimetypes -import requests import zipfile -import shutil import re from PIL import Image, ImageDraw -API_HEADERS = { - 'X-API-KEY': '123456789' -} +API_HEADERS = {"X-API-KEY": "123456789"} ######### # GIVEN # ######### + @given('I generate a PDF file as "{fileInput}"') def step_generate_pdf(context, fileInput): context.param_name = fileInput context.file_name = "genericNonCustomisableName.pdf" writer = PdfWriter() writer.add_blank_page(width=72, height=72) # Single blank page - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: writer.write(f) - if not hasattr(context, 'files'): + if not hasattr(context, "files"): context.files = {} - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") @given('I use an example file at "{filePath}" as parameter "{fileInput}"') def step_use_example_file(context, filePath, fileInput): context.param_name = fileInput - context.file_name = filePath.split('/')[-1] - if not hasattr(context, 'files'): + context.file_name = filePath.split("/")[-1] + if not hasattr(context, "files"): context.files = {} # Ensure the file exists before opening try: - example_file = open(filePath, 'rb') + example_file = open(filePath, "rb") context.files[context.param_name] = example_file except FileNotFoundError: raise FileNotFoundError(f"The example file '{filePath}' does not exist.") -@given('the pdf contains {page_count:d} pages') + +@given("the pdf contains {page_count:d} pages") def step_pdf_contains_pages(context, page_count): writer = PdfWriter() for i in range(page_count): writer.add_blank_page(width=72, height=72) - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: writer.write(f) context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") + # Duplicate for now... -@given('the pdf contains {page_count:d} blank pages') +@given("the pdf contains {page_count:d} blank pages") def step_pdf_contains_blank_pages(context, page_count): writer = PdfWriter() for i in range(page_count): writer.add_blank_page(width=72, height=72) - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: writer.write(f) context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") + def create_black_box_image(file_name, size): can = canvas.Canvas(file_name, pagesize=size) @@ -80,14 +80,20 @@ def create_black_box_image(file_name, size): can.showPage() can.save() -@given(u'the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages') + +@given( + "the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages" +) def step_impl(context, image_count, width, height, page_count): context.param_name = "fileInput" context.file_name = "genericNonCustomisableName.pdf" - create_pdf_with_images_and_boxes(context.file_name, image_count, page_count, width, height) - if not hasattr(context, 'files'): + create_pdf_with_images_and_boxes( + context.file_name, image_count, page_count, width, height + ) + if not hasattr(context, "files"): context.files = {} - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") + def add_black_boxes_to_image(image): if isinstance(image, str): @@ -97,9 +103,14 @@ def add_black_boxes_to_image(image): draw.rectangle([(0, 0), image.size], fill=(0, 0, 0)) # Fill image with black return image -def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_width, image_height): + +def create_pdf_with_images_and_boxes( + file_name, image_count, page_count, image_width, image_height +): page_width, page_height = max(letter[0], image_width), max(letter[1], image_height) - boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0) + boxes_per_page = image_count // page_count + ( + 1 if image_count % page_count != 0 else 0 + ) writer = PdfWriter() box_counter = 0 @@ -114,12 +125,14 @@ def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_w # Simulating a dynamic image creation (replace this with your actual image creation logic) # For demonstration, we'll create a simple black image - dummy_image = Image.new('RGB', (image_width, image_height), color='white') # Create a white image + dummy_image = Image.new( + "RGB", (image_width, image_height), color="white" + ) # Create a white image dummy_image = add_black_boxes_to_image(dummy_image) # Add black boxes # Convert the PIL Image to bytes to pass to drawImage image_bytes = io.BytesIO() - dummy_image.save(image_bytes, format='PNG') + dummy_image.save(image_bytes, format="PNG") image_bytes.seek(0) # Check if the image fits in the current page dimensions @@ -130,7 +143,9 @@ def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_w break # Add the image to the PDF - can.drawImage(ImageReader(image_bytes), x, y, width=image_width, height=image_height) + can.drawImage( + ImageReader(image_bytes), x, y, width=image_width, height=image_height + ) box_counter += 1 can.showPage() @@ -140,7 +155,7 @@ def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_w writer.add_page(new_pdf.pages[0]) # Write the PDF to file - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: writer.write(f) # Clean up temporary image files @@ -149,36 +164,81 @@ def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_w if os.path.exists(temp_image_path): os.remove(temp_image_path) -@given('the pdf contains {image_count:d} images on {page_count:d} pages') + +@given("the pdf contains {image_count:d} images on {page_count:d} pages") def step_pdf_contains_images(context, image_count, page_count): - if not hasattr(context, 'param_name'): + if not hasattr(context, "param_name"): context.param_name = "default" context.file_name = "genericNonCustomisableName.pdf" create_pdf_with_black_boxes(context.file_name, image_count, page_count) - if not hasattr(context, 'files'): + if not hasattr(context, "files"): context.files = {} if context.param_name in context.files: context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") -@given('the pdf contains {page_count:d} pages with random text') + +def create_pdf_with_black_boxes(file_name, image_count, page_count): + + page_width, page_height = letter + writer = PdfWriter() + box_counter = 0 + + for page in range(page_count): + packet = io.BytesIO() + can = canvas.Canvas(packet, pagesize=(page_width, page_height)) + + boxes_per_page = image_count // page_count + ( + 1 if image_count % page_count != 0 else 0 + ) + for i in range(boxes_per_page): + if box_counter >= image_count: + break + + # Create a black box image + dummy_image = Image.new("RGB", (100, 100), color="black") + image_bytes = io.BytesIO() + dummy_image.save(image_bytes, format="PNG") + image_bytes.seek(0) + + x = (i % (page_width // 100)) * 100 + y = page_height - (((i % (page_height // 100)) + 1) * 100) + + if x + 100 > page_width or y < 0: + break + + can.drawImage(ImageReader(image_bytes), x, y, width=100, height=100) + box_counter += 1 + + can.showPage() + can.save() + packet.seek(0) + new_pdf = PdfReader(packet) + writer.add_page(new_pdf.pages[0]) + + with open(file_name, "wb") as f: + writer.write(f) + + +@given("the pdf contains {page_count:d} pages with random text") def step_pdf_contains_pages_with_random_text(context, page_count): buffer = io.BytesIO() c = canvas.Canvas(buffer, pagesize=letter) width, height = letter for _ in range(page_count): - text = ''.join(random.choices(string.ascii_letters + string.digits, k=100)) + text = "".join(random.choices(string.ascii_letters + string.digits, k=100)) c.drawString(100, height - 100, text) c.showPage() c.save() - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: f.write(buffer.getvalue()) context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") + @given('the pdf pages all contain the text "{text}"') def step_pdf_pages_contain_text(context, text): @@ -192,11 +252,12 @@ def step_pdf_pages_contain_text(context, text): c.save() - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: f.write(buffer.getvalue()) context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") + @given('the pdf is encrypted with password "{password}"') def step_encrypt_pdf(context, password): @@ -205,29 +266,34 @@ def step_encrypt_pdf(context, password): for i in range(len(reader.pages)): writer.add_page(reader.pages[i]) writer.encrypt(password) - with open(context.file_name, 'wb') as f: + with open(context.file_name, "wb") as f: writer.write(f) context.files[context.param_name].close() - context.files[context.param_name] = open(context.file_name, 'rb') + context.files[context.param_name] = open(context.file_name, "rb") -@given('the request data is') + +@given("the request data is") def step_request_data(context): context.request_data = eval(context.text) -@given('the request data includes') + +@given("the request data includes") def step_request_data_table(context): - context.request_data = {row['parameter']: row['value'] for row in context.table} + context.request_data = {row["parameter"]: row["value"] for row in context.table} + @given('save the generated PDF file as "{filename}" for debugging') def save_generated_pdf(context, filename): - with open(filename, 'wb') as f: + with open(filename, "wb") as f: f.write(context.files[context.param_name].read()) print(f"Saved generated PDF content to {filename}") + ######## # WHEN # ######## + @when('I send a GET request to "{endpoint}"') def step_send_get_request(context, endpoint): base_url = "http://localhost:8080" @@ -235,20 +301,22 @@ def step_send_get_request(context, endpoint): response = requests.get(full_url, headers=API_HEADERS) context.response = response + @when('I send a GET request to "{endpoint}" with parameters') def step_send_get_request_with_params(context, endpoint): base_url = "http://localhost:8080" - params = {row['parameter']: row['value'] for row in context.table} + params = {row["parameter"]: row["value"] for row in context.table} full_url = f"{base_url}{endpoint}" response = requests.get(full_url, params=params, headers=API_HEADERS) context.response = response + @when('I send the API request to the endpoint "{endpoint}"') def step_send_api_request(context, endpoint): url = f"http://localhost:8080{endpoint}" - files = context.files if hasattr(context, 'files') else {} + files = context.files if hasattr(context, "files") else {} - if not hasattr(context, 'request_data') or context.request_data is None: + if not hasattr(context, "request_data") or context.request_data is None: context.request_data = {} form_data = [] @@ -257,130 +325,173 @@ def step_send_api_request(context, endpoint): for key, file in files.items(): mime_type, _ = mimetypes.guess_type(file.name) - mime_type = mime_type or 'application/octet-stream' + mime_type = mime_type or "application/octet-stream" print(f"form_data {file.name} with {mime_type}") form_data.append((key, (file.name, file, mime_type))) response = requests.post(url, files=form_data, headers=API_HEADERS) context.response = response + ######## # THEN # ######## + @then('the response content type should be "{content_type}"') def step_check_response_content_type(context, content_type): - actual_content_type = context.response.headers.get('Content-Type', '') - assert actual_content_type.startswith(content_type), f"Expected {content_type} but got {actual_content_type}. Response content: {context.response.content}" + actual_content_type = context.response.headers.get("Content-Type", "") + assert actual_content_type.startswith( + content_type + ), f"Expected {content_type} but got {actual_content_type}. Response content: {context.response.content}" -@then('the response file should have size greater than {size:d}') + +@then("the response file should have size greater than {size:d}") def step_check_response_file_size(context, size): response_file = io.BytesIO(context.response.content) assert len(response_file.getvalue()) > size -@then('the response PDF is not passworded') + +@then("the response PDF is not passworded") def step_check_response_pdf_not_passworded(context): response_file = io.BytesIO(context.response.content) reader = PdfReader(response_file) assert not reader.is_encrypted -@then('the response PDF is passworded') + +@then("the response PDF is passworded") def step_check_response_pdf_passworded(context): response_file = io.BytesIO(context.response.content) try: reader = PdfReader(response_file) assert reader.is_encrypted except PdfReadError as e: - raise AssertionError(f"Failed to read PDF: {str(e)}. Response content: {context.response.content}") + raise AssertionError( + f"Failed to read PDF: {str(e)}. Response content: {context.response.content}" + ) except Exception as e: - raise AssertionError(f"An error occurred: {str(e)}. Response content: {context.response.content}") + raise AssertionError( + f"An error occurred: {str(e)}. Response content: {context.response.content}" + ) -@then('the response status code should be {status_code:d}') + +@then("the response status code should be {status_code:d}") def step_check_response_status_code(context, status_code): - assert context.response.status_code == status_code, f"Expected status code {status_code} but got {context.response.status_code}" + assert ( + context.response.status_code == status_code + ), f"Expected status code {status_code} but got {context.response.status_code}" + @then('the response should contain error message "{message}"') def step_check_response_error_message(context, message): response_json = context.response.json() - assert response_json.get('error') == message, f"Expected error message '{message}' but got '{response_json.get('error')}'" + assert ( + response_json.get("error") == message + ), f"Expected error message '{message}' but got '{response_json.get('error')}'" -@then('the response PDF should contain {page_count:d} pages') -def step_check_response_pdf_page_count(context, page_count): - response_file = io.BytesIO(context.response.content) - reader = PdfReader(response_file) - assert len(reader.pages) == page_count, f"Expected {page_count} pages but got {len(reader.pages)} pages" @then('the response PDF metadata should include "{metadata_key}" as "{metadata_value}"') def step_check_response_pdf_metadata(context, metadata_key, metadata_value): response_file = io.BytesIO(context.response.content) reader = PdfReader(response_file) metadata = reader.metadata - assert metadata.get("/" + metadata_key) == metadata_value, f"Expected {metadata_key} to be '{metadata_value}' but got '{metadata.get(metadata_key)}'" + assert ( + metadata.get("/" + metadata_key) == metadata_value + ), f"Expected {metadata_key} to be '{metadata_value}' but got '{metadata.get(metadata_key)}'" + @then('the response file should have extension "{extension}"') def step_check_response_file_extension(context, extension): - content_disposition = context.response.headers.get('Content-Disposition', '') + content_disposition = context.response.headers.get("Content-Disposition", "") filename = "" if content_disposition: - parts = content_disposition.split(';') + parts = content_disposition.split(";") for part in parts: - if part.strip().startswith('filename'): - filename = part.split('=')[1].strip().strip('"') + if part.strip().startswith("filename"): + filename = part.split("=")[1].strip().strip('"') break - assert filename.endswith(extension), f"Expected file extension {extension} but got {filename}. Response content: {context.response.content}" + assert filename.endswith( + extension + ), f"Expected file extension {extension} but got {filename}. Response content: {context.response.content}" + @then('save the response file as "{filename}" for debugging') def step_save_response_file(context, filename): - with open(filename, 'wb') as f: + with open(filename, "wb") as f: f.write(context.response.content) print(f"Saved response content to {filename}") -@then('the response PDF should contain {page_count:d} pages') + +@then("the response PDF should contain {page_count:d} pages") def step_check_response_pdf_page_count(context, page_count): response_file = io.BytesIO(context.response.content) reader = PdfReader(io.BytesIO(response_file.getvalue())) actual_page_count = len(reader.pages) - assert actual_page_count == page_count, f"Expected {page_count} pages but got {actual_page_count} pages" + assert ( + actual_page_count == page_count + ), f"Expected {page_count} pages but got {actual_page_count} pages" -@then('the response ZIP should contain {file_count:d} files') + +@then("the response ZIP should contain {file_count:d} files") def step_check_response_zip_file_count(context, file_count): response_file = io.BytesIO(context.response.content) with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file: actual_file_count = len(zip_file.namelist()) - assert actual_file_count == file_count, f"Expected {file_count} files but got {actual_file_count} files" + assert ( + actual_file_count == file_count + ), f"Expected {file_count} files but got {actual_file_count} files" -@then('the response ZIP file should contain {doc_count:d} documents each having {pages_per_doc:d} pages') + +@then( + "the response ZIP file should contain {doc_count:d} documents each having {pages_per_doc:d} pages" +) def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc): response_file = io.BytesIO(context.response.content) with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file: actual_doc_count = len(zip_file.namelist()) - assert actual_doc_count == doc_count, f"Expected {doc_count} documents but got {actual_doc_count} documents" + assert ( + actual_doc_count == doc_count + ), f"Expected {doc_count} documents but got {actual_doc_count} documents" for file_name in zip_file.namelist(): with zip_file.open(file_name) as pdf_file: reader = PdfReader(pdf_file) actual_pages_per_doc = len(reader.pages) - assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}" + assert ( + actual_pages_per_doc == pages_per_doc + ), f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}" + @then('the JSON value of "{key}" should be "{expected_value}"') def step_check_json_value(context, key, expected_value): actual_value = context.response.json().get(key) - assert actual_value == expected_value, \ - f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'" + assert ( + actual_value == expected_value + ), f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'" -@then('JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"') -def step_check_json_list_entry(context, identifier_key, identifier_self, target_key, target_value): + +@then( + 'JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"' +) +def step_check_json_list_entry( + context, identifier_key, identifier_self, target_key, target_value +): json_response = context.response.json() for entry in json_response: if entry.get(identifier_key) == identifier_value: - assert entry.get(target_key) == target_value, \ - f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}" + assert ( + entry.get(target_key) == target_value + ), f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}" break else: - raise AssertionError(f"No entry with {identifier_key} as {identifier_value} found") + raise AssertionError( + f"No entry with {identifier_key} as {identifier_value} found" + ) + @then('the response should match the regex "{pattern}"') def step_response_matches_regex(context, pattern): response_text = context.response.text - assert re.match(pattern, response_text), \ - f"Response '{response_text}' does not match the expected pattern '{pattern}'" + assert re.match( + pattern, response_text + ), f"Response '{response_text}' does not match the expected pattern '{pattern}'" From 8ba7cfe92178aa59906759ff09acd466bb6b2293 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:36:21 +0100 Subject: [PATCH 08/12] Bump com.unboundid.product.scim2:scim2-sdk-client from 2.3.5 to 4.0.0 (#3736) Bumps [com.unboundid.product.scim2:scim2-sdk-client](https://github.com/pingidentity/scim2) from 2.3.5 to 4.0.0.
Changelog

Sourced from com.unboundid.product.scim2:scim2-sdk-client's changelog.

v4.0.0 - 2025-Jun-10

Removed support for Java 11. The UnboundID SCIM 2 SDK now requires Java 17 or a later release.

Updated the following dependencies:

  • Jackson: 2.18.3
  • Jakarta RS: 4.0.0
  • Jersey: 3.1.10

Updated the default behavior for ADD patch requests with value filters (e.g., emails[type eq "work"].display). The SCIM SDK will now target existing values within the multi-valued attribute. For more background on this type of patch request, see the release notes for the 3.2.0 release where this was introduced (but not made the default). To restore the old behavior, set the following property in your application:

PatchOperation.APPEND_NEW_PATCH_VALUES_PROPERTY = true;

Updated SearchRequestBuilder to be more permissive of ListResponses with non-standard attribute casing (e.g., if a response includes a "resources" array instead of "Resources").

Updated the class-level documentation of SearchRequest to provide more background about how searches are performed in the SCIM standard.

Added a new property that allows ignoring unknown fields when converting JSON text to Java objects that inherit from BaseScimResource. This behaves similarly to the FAIL_ON_UNKNOWN_PROPERTIES setting from the Jackson library, and allows for easier integration with SCIM service providers that include additional non-standard data in their responses. To enable this setting, set the following property in your application code:

BaseScimResource.IGNORE_UNKNOWN_FIELDS = true;

Fixed an issue with methods that interface with schema extensions such as BaseScimResource.getExtensionValues(String). These accepted paths as a string, but previously performed updates to the extension data incorrectly.

Simplified the implementation of the StaticUtils#toLowerCase method. This had an optimization for Java versions before JDK 9 that was especially beneficial for the most common case of handling ASCII characters. Since JDK 9, however, the String class has been updated so that the class is backed by a byte array as opposed to a character array, so it is more optimal to use the JDK's implementation directly while handling null values.

Previous releases of the SCIM SDK set many classes as final to encourage applications to follow strict compliance to the SCIM standard. However, this also makes it difficult to integrate with services that violate the standard. An example of this is a SCIM error response that contains extra fields in the JSON body. To help accommodate these integrations, the SCIM SDK has been updated so that several model classes are no longer final, allowing applications to extend them if needed. The following classes were updated:

  • scim2-sdk-client builder classes such as CreateRequestBuilder.java
  • ErrorResponse.java

... (truncated)

Commits
  • 039c7e6 Setting release version 4.0.0
  • ea04864 Update CHANGELOG date for the 4.0.0 release.
  • bfd276e Make GenericScimResource extendable.
  • 9008757 Clean up POM and remove Guava test dependency.
  • a954381 Remove the deprecated ScimDateFormat class.
  • 76f2314 Enhance the Filter classes and their documentation
  • cfd9d7e Add a new filter method for SearchRequestBuilder.
  • 3c3c0ca Fix CodeQL by adding Java 17 installation step
  • 114ad51 Import the default codeql.yaml
  • 26fe8f1 Allow extending model classes
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.unboundid.product.scim2:scim2-sdk-client&package-manager=gradle&previous-version=2.3.5&new-version=4.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- proprietary/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proprietary/build.gradle b/proprietary/build.gradle index 1912eefcb..2a72f8a65 100644 --- a/proprietary/build.gradle +++ b/proprietary/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' api 'io.micrometer:micrometer-registry-prometheus' - implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' + implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0' runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database runtimeOnly 'org.postgresql:postgresql:42.7.7' constraints { From 626734c781263176462dae10995e2c71afcf55e7 Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 14 Jul 2025 13:37:03 +0200 Subject: [PATCH 09/12] chore: add integrate Stylelint for CSS linting (#3909) # Description of Changes **What was changed** - Added a new `.stylelintrc.json` to configure Stylelint with `stylelint-config-standard` and custom ignore rules. - Created a `lint:css` script in `package.json` and added `stylelint`/`stylelint-config-standard` to `devDependencies`. - Added `package-lock.json` to lock dependencies. - Updated numerous CSS files under `stirling-pdf/src/main/resources/static/css/` to fix lint errors (shorthand properties, removed redundant units, consistent box-shadow syntax, margin shorthand, etc.). **Why the change was made** - To enforce consistent, modern CSS code style across the project, catch errors early, and enable automated fixing of common lint issues. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .devcontainer/devcontainer.json | 4 +- .github/labeler-config-srvaroa.yml | 1 + .vscode/extensions.json | 2 + .vscode/settings.json | 7 + devGuide/STYLELINT.md | 47 + devTools/.stylelintrc.json | 69 ++ devTools/package-lock.json | 1598 ++++++++++++++++++++++++++++ devTools/package.json | 13 + 8 files changed, 1740 insertions(+), 1 deletion(-) create mode 100644 devGuide/STYLELINT.md create mode 100644 devTools/.stylelintrc.json create mode 100644 devTools/package-lock.json create mode 100644 devTools/package.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 644378d12..5ab9f82c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -119,7 +119,9 @@ "EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles "ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code "charliermarsh.ruff", // Ruff extension for Ruff language support - "github.vscode-github-actions" // GitHub Actions extension for Visual Studio Code + "github.vscode-github-actions", // GitHub Actions extension for Visual Studio Code + "stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting + "redhat.vscode-yaml" // YAML extension for Visual Studio Code ] } }, diff --git a/.github/labeler-config-srvaroa.yml b/.github/labeler-config-srvaroa.yml index e37a8a810..06368536f 100644 --- a/.github/labeler-config-srvaroa.yml +++ b/.github/labeler-config-srvaroa.yml @@ -126,6 +126,7 @@ labels: - '.pre-commit-config' - '.github/workflows/pre_commit.yml' - 'devGuide/.*' + - 'devTools/.*' - label: 'Test' files: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 128af83ba..6ab09796f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -17,5 +17,7 @@ "GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code "charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide "yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing + "stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting + "redhat.vscode-yaml", // YAML extension for Visual Studio Code ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f272e18a..03d51b765 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,9 @@ "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, + "[css]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, @@ -27,6 +30,9 @@ "[gradle]": { "editor.defaultFormatter": "vscjava.vscode-gradle" }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, "java.compile.nullAnalysis.mode": "automatic", "java.configuration.updateBuildConfiguration": "interactive", "java.format.enabled": true, @@ -119,6 +125,7 @@ "html.format.indentHandlebars": true, "html.format.preserveNewLines": true, "html.format.maxPreserveNewLines": 2, + "stylelint.configFile": "devTools/.stylelintrc.json", "java.project.sourcePaths": [ "stirling-pdf/src/main/java", "common/src/main/java", diff --git a/devGuide/STYLELINT.md b/devGuide/STYLELINT.md new file mode 100644 index 000000000..27bc4da4d --- /dev/null +++ b/devGuide/STYLELINT.md @@ -0,0 +1,47 @@ +# STYLELINT.md + +## Usage + +Apply Stylelint to your project's CSS with the following steps: + +1. **NPM Script** + + - Go to directory: `devTools/` + + - Add Stylelint & stylistic/stylelint-plugin + ```bash + npm install --save-dev stylelint stylelint-config-standard + npm install --save-dev @stylistic/stylelint-plugin + ``` + - Add a script entry to your `package.json`: + ```jsonc + { + "scripts": { + "lint:css:check": "stylelint \"../stirling-pdf/src/main/**/*.css\" \"../proprietary/src/main/resources/static/css/*.css\" --config .stylelintrc.json", + "lint:css:fix": "stylelint \"../stirling-pdf/src/main/**/*.css\" \"../proprietary/src/main/resources/static/css/*.css\" --config .stylelintrc.json --fix" + } + } + ``` + - Run the linter: + ```bash + npm run lint:css:check + npm run lint:css:fix + ``` + +2. **CLI Usage** + + - Lint all CSS files: + ```bash + npx stylelint ../stirling-pdf/src/main/**/*.css ../proprietary/src/main/resources/static/css/*.css + ``` + - Lint a single file: + ```bash + npx stylelint ../proprietary/src/main/resources/static/css/audit-dashboard.css + ``` + - Apply automatic fixes: + ```bash + npx stylelint "../stirling-pdf/src/main/**/*.css" "../proprietary/src/main/resources/static/css/*.css" --fix + ``` + +For full configuration options and rule customization, refer to the official documentation: [https://stylelint.io](https://stylelint.io) + diff --git a/devTools/.stylelintrc.json b/devTools/.stylelintrc.json new file mode 100644 index 000000000..d676c0159 --- /dev/null +++ b/devTools/.stylelintrc.json @@ -0,0 +1,69 @@ +{ + "extends": [ + "stylelint-config-standard" + ], + "plugins": [ + "@stylistic/stylelint-plugin" + ], + "ignoreFiles": [ + "stirling-pdf/src/main/resources/static/css/bootstrap*.css", + "stirling-pdf/src/main/resources/static/css/cookieconsent.css", + "stirling-pdf/src/main/resources/static/css/cookieconsentCustomisation.css", + "stirling-pdf/src/main/resources/static/css/prism.css", + "stirling-pdf/src/main/resources/static/pdfjs-legacy/**/*.css" + ], + "rules": { + "property-no-vendor-prefix": null, + "value-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "media-feature-name-no-vendor-prefix": null, + "value-keyword-case": null, + "color-function-notation": null, + "alpha-value-notation": null, + "color-function-alias-notation": null, + "selector-class-pattern": null, + "selector-id-pattern": null, + "declaration-block-no-redundant-longhand-properties": null, + "media-feature-range-notation": "prefix", + "selector-attribute-quotes": null, + "at-rule-no-vendor-prefix": null, + "selector-not-notation": null, + "no-duplicate-selectors": [ + true, + { + "disableFix": true + } + ], + "comment-word-disallowed-list": null, + "custom-property-pattern": null, + "no-descending-specificity": null, + "keyframes-name-pattern": null, + "comment-empty-line-before": [ + "always", + { + "ignore": [ + "stylelint-commands" + ] + } + ], + "block-no-empty": true, + "@stylistic/declaration-bang-space-after": "never", + "@stylistic/declaration-bang-space-before": "always", + "@stylistic/declaration-block-trailing-semicolon": "always", + "@stylistic/function-comma-space-after": [ + "always-single-line", + { + "disableFix": false + } + ], + "@stylistic/function-comma-space-before": "never", + "@stylistic/color-hex-case": "lower", + "@stylistic/declaration-block-semicolon-newline-after": "always", + "@stylistic/indentation": [ + 2, + { + "baseIndentLevel": 2 + } + ] + } +} diff --git a/devTools/package-lock.json b/devTools/package-lock.json new file mode 100644 index 000000000..da6cfe0ca --- /dev/null +++ b/devTools/package-lock.json @@ -0,0 +1,1598 @@ +{ + "name": "stirling-pdf", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stirling-pdf", + "version": "1.0.0", + "devDependencies": { + "@stylistic/stylelint-plugin": "^3.1.3", + "stylelint": "^16.21.1", + "stylelint-config-standard": "^38.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@stylistic/stylelint-plugin": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.3.tgz", + "integrity": "sha512-85fsmzgsIVmyG3/GFrjuYj6Cz8rAM7IZiPiXCMiSMfoDOC1lOrzrXPDk24WqviAghnPqGpx8b0caK2PuewWGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1", + "@csstools/media-query-list-parser": "^3.0.1", + "is-plain-object": "^5.0.0", + "postcss": "^8.4.41", + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0", + "style-search": "^0.1.0" + }, + "engines": { + "node": "^18.12 || >=20.9" + }, + "peerDependencies": { + "stylelint": "^16.8.0" + } + }, + "node_modules/@stylistic/stylelint-plugin/node_modules/@csstools/media-query-list-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" + } + }, + "node_modules/@stylistic/stylelint-plugin/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cacheable": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.1.tgz", + "integrity": "sha512-Fa2BZY0CS9F0PFc/6aVA6tgpOdw+hmv9dkZOlHXII5v5Hw+meJBIWDcPrG9q/dXxGcNbym5t77fzmawrBQfTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.10.0", + "keyv": "^5.3.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12 || >=16" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", + "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.11.tgz", + "integrity": "sha512-zfOAns94mp7bHG/vCn9Ru2eDCmIxVQ5dELUHKjHfDEOJmHNzE+uGa6208kfkgmtym4a0FFjEuFksCXFacbVhSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^1.10.1", + "flatted": "^3.3.3", + "hookified": "^1.10.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hookified": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.10.0.tgz", + "integrity": "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", + "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.0.3" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/stylelint": { + "version": "16.21.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.21.1.tgz", + "integrity": "sha512-WCXdXnYK2tpCbebgMF0Bme3YZH/Rh/UXerj75twYo4uLULlcrLwFVdZTvTEF8idFnAcW21YUDJFyKOfaf6xJRw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.1.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.1", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^10.1.1", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-16.0.0.tgz", + "integrity": "sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.16.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-38.0.0.tgz", + "integrity": "sha512-uj3JIX+dpFseqd/DJx8Gy3PcRAJhlEZ2IrlFOc4LUxBX/PNMEQ198x7LCOE2Q5oT9Vw8nyc4CIL78xSqPr6iag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "stylelint-config-recommended": "^16.0.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + } + } +} diff --git a/devTools/package.json b/devTools/package.json new file mode 100644 index 000000000..e58ed4df0 --- /dev/null +++ b/devTools/package.json @@ -0,0 +1,13 @@ +{ + "name": "stirling-pdf", + "version": "1.0.0", + "scripts": { + "lint:css:check": "stylelint \"../stirling-pdf/src/main/**/*.css\" \"../proprietary/src/main/resources/static/css/*.css\" --config .stylelintrc.json", + "lint:css:fix": "stylelint \"../stirling-pdf/src/main/**/*.css\" \"../proprietary/src/main/resources/static/css/*.css\" --config .stylelintrc.json --fix" + }, + "devDependencies": { + "@stylistic/stylelint-plugin": "^3.1.3", + "stylelint": "^16.21.1", + "stylelint-config-standard": "^38.0.0" + } +} From 60cb610d247ac4c793d8da268653d70625f5ceee Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:38:47 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#3942)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../stirling/software/common/service/TaskManagerTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/test/java/stirling/software/common/service/TaskManagerTest.java b/common/src/test/java/stirling/software/common/service/TaskManagerTest.java index b2cb26dd8..5fd2dcc87 100644 --- a/common/src/test/java/stirling/software/common/service/TaskManagerTest.java +++ b/common/src/test/java/stirling/software/common/service/TaskManagerTest.java @@ -95,10 +95,10 @@ class TaskManagerTest { assertTrue(result.isComplete()); assertTrue(result.hasFiles()); assertFalse(result.hasMultipleFiles()); - + var resultFiles = result.getAllResultFiles(); assertEquals(1, resultFiles.size()); - + ResultFile resultFile = resultFiles.get(0); assertEquals(fileId, resultFile.getFileId()); assertEquals(originalFileName, resultFile.getFileName()); @@ -180,7 +180,7 @@ class TaskManagerTest { // Arrange // Mock fileStorage.getFileSize for file operations when(fileStorage.getFileSize("file-id")).thenReturn(1024L); - + // 1. Create active job String activeJobId = "active-job"; taskManager.createTask(activeJobId); @@ -232,7 +232,7 @@ class TaskManagerTest { LocalDateTime oldTime = LocalDateTime.now().minusHours(1); ReflectionTestUtils.setField(oldJob, "completedAt", oldTime); ReflectionTestUtils.setField(oldJob, "complete", true); - + // Create a ResultFile and set it using the new approach ResultFile resultFile = ResultFile.builder() .fileId("file-id") From 357d8a7d381637e9913ce61014ce83f32c15edb4 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:39:03 +0100 Subject: [PATCH 11/12] :globe_with_meridians: Sync Translations + Update README Progress Table (#3918) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 4 ++-- scripts/ignore_translation.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0909ba2f..02712a1ad 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Stirling-PDF currently supports 40 languages! | Indonesian (Bahasa Indonesia) (id_ID) | ![63%](https://geps.dev/progress/63) | | Irish (Gaeilge) (ga_IE) | ![70%](https://geps.dev/progress/70) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | -| Japanese (日本語) (ja_JP) | ![70%](https://geps.dev/progress/70) | +| Japanese (日本語) (ja_JP) | ![95%](https://geps.dev/progress/95) | | Korean (한국어) (ko_KR) | ![69%](https://geps.dev/progress/69) | | Norwegian (Norsk) (no_NB) | ![67%](https://geps.dev/progress/67) | | Persian (فارسی) (fa_IR) | ![66%](https://geps.dev/progress/66) | @@ -145,7 +145,7 @@ Stirling-PDF currently supports 40 languages! | Romanian (Română) (ro_RO) | ![59%](https://geps.dev/progress/59) | | Russian (Русский) (ru_RU) | ![70%](https://geps.dev/progress/70) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![97%](https://geps.dev/progress/97) | -| Simplified Chinese (简体中文) (zh_CN) | ![90%](https://geps.dev/progress/90) | +| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) | | Slovakian (Slovensky) (sk_SK) | ![53%](https://geps.dev/progress/53) | | Slovenian (Slovenščina) (sl_SI) | ![73%](https://geps.dev/progress/73) | | Spanish (Español) (es_ES) | ![75%](https://geps.dev/progress/75) | diff --git a/scripts/ignore_translation.toml b/scripts/ignore_translation.toml index 01f1ae1f0..3773308d4 100644 --- a/scripts/ignore_translation.toml +++ b/scripts/ignore_translation.toml @@ -529,7 +529,6 @@ ignore = [ [ja_JP] ignore = [ - 'lang.jav', 'language.direction', ] From 38b53d7cc19ed406d772d71f881391a2ef42c439 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:19:17 +0100 Subject: [PATCH 12/12] Update 3rd Party Licenses (#3943) Auto-generated by stirlingbot[bot] Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../resources/static/3rdPartyLicenses.json | 62 ------------------- 1 file changed, 62 deletions(-) diff --git a/stirling-pdf/src/main/resources/static/3rdPartyLicenses.json b/stirling-pdf/src/main/resources/static/3rdPartyLicenses.json index a7d899263..440cdb265 100644 --- a/stirling-pdf/src/main/resources/static/3rdPartyLicenses.json +++ b/stirling-pdf/src/main/resources/static/3rdPartyLicenses.json @@ -165,12 +165,6 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "com.google.errorprone:error_prone_annotations", - "moduleVersion": "2.11.0", - "moduleLicense": "Apache 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" - }, { "moduleName": "com.google.errorprone:error_prone_annotations", "moduleUrl": "https://errorprone.info/error_prone_annotations", @@ -639,13 +633,6 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", - "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", - "moduleVersion": "2.2.30", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", @@ -653,13 +640,6 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, - { - "moduleName": "io.swagger.core.v3:swagger-core-jakarta", - "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", - "moduleVersion": "2.2.30", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", @@ -667,13 +647,6 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, - { - "moduleName": "io.swagger.core.v3:swagger-models-jakarta", - "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", - "moduleVersion": "2.2.30", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", @@ -744,13 +717,6 @@ "moduleLicense": "GPL2 w/ CPE", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, - { - "moduleName": "jakarta.servlet:jakarta.servlet-api", - "moduleUrl": "https://www.eclipse.org", - "moduleVersion": "6.1.0", - "moduleLicense": "GPL2 w/ CPE", - "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" - }, { "moduleName": "jakarta.transaction:jakarta.transaction-api", "moduleUrl": "https://projects.eclipse.org/projects/ee4j.jta", @@ -889,13 +855,6 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "org.apache.commons:commons-text", - "moduleUrl": "https://commons.apache.org/proper/commons-text", - "moduleVersion": "1.10.0", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" - }, { "moduleName": "org.apache.commons:commons-text", "moduleUrl": "https://commons.apache.org/proper/commons-text", @@ -1018,13 +977,6 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "org.bouncycastle:bcpkix-jdk18on", - "moduleUrl": "https://www.bouncycastle.org/java.html", - "moduleVersion": "1.72", - "moduleLicense": "Bouncy Castle Licence", - "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" - }, { "moduleName": "org.bouncycastle:bcpkix-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", @@ -1039,13 +991,6 @@ "moduleLicense": "Bouncy Castle Licence", "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" }, - { - "moduleName": "org.bouncycastle:bcutil-jdk18on", - "moduleUrl": "https://www.bouncycastle.org/java.html", - "moduleVersion": "1.72", - "moduleLicense": "Bouncy Castle Licence", - "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" - }, { "moduleName": "org.bouncycastle:bcutil-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", @@ -1562,13 +1507,6 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, - { - "moduleName": "org.springframework.boot:spring-boot-devtools", - "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.3", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" - }, { "moduleName": "org.springframework.boot:spring-boot-starter", "moduleUrl": "https://spring.io/projects/spring-boot",