diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..adaa9a5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + frontend: + name: Frontend build + runs-on: ubuntu-latest + + defaults: + run: + working-directory: src/frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: src/frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build frontend + run: npm run build + + backend: + name: Backend build and tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Restore backend + run: dotnet restore ThesisValidator.sln + + - name: Build backend + run: dotnet build src/backend/ThesisValidator.Api.csproj --configuration Release --no-restore + + - name: Run backend tests + run: dotnet test tests/backend.Tests/ThesisValidator.Api.Tests.csproj --configuration Release --no-restore --verbosity normal diff --git a/.gitignore b/.gitignore index 63ce472..59f437b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,75 +1,13 @@ -/backend.Tests/bin +tests/backend.Tests/bin +/.dotnet/ +/.dotnet-home/ +/.codex-* /.idea -/backend.Tests/obj -/backend/.idea -/backend/bin -/backend/obj -backend/output/hello_validated_20260201_085806.docx -backend/output/test_validated_20260130_023857.zip -backend/output/test_validated_20260131_190122.docx -backend/output/test_validated_20260201_085634.docx -backend/output/test_validated_20260130_023857/\[Content_Types].xml -backend/output/test_validated_20260130_023857/hello.docx -backend/output/test_validated_20260130_023857/_rels/.rels -backend/output/test_validated_20260130_023857/customXml/item1.xml -backend/output/test_validated_20260130_023857/customXml/item2.xml -backend/output/test_validated_20260130_023857/customXml/item3.xml -backend/output/test_validated_20260130_023857/customXml/item4.xml -backend/output/test_validated_20260130_023857/customXml/itemProps1.xml -backend/output/test_validated_20260130_023857/customXml/itemProps2.xml -backend/output/test_validated_20260130_023857/customXml/itemProps3.xml -backend/output/test_validated_20260130_023857/customXml/itemProps4.xml -backend/output/test_validated_20260130_023857/customXml/_rels/item1.xml.rels -backend/output/test_validated_20260130_023857/customXml/_rels/item2.xml.rels -backend/output/test_validated_20260130_023857/customXml/_rels/item3.xml.rels -backend/output/test_validated_20260130_023857/customXml/_rels/item4.xml.rels -backend/output/test_validated_20260130_023857/docProps/app.xml -backend/output/test_validated_20260130_023857/docProps/core.xml -backend/output/test_validated_20260130_023857/docProps/custom.xml -backend/output/test_validated_20260130_023857/hello/\[Content_Types].xml -backend/output/test_validated_20260130_023857/hello/_rels/.rels -backend/output/test_validated_20260130_023857/hello/customXml/item1.xml -backend/output/test_validated_20260130_023857/hello/customXml/itemProps1.xml -backend/output/test_validated_20260130_023857/hello/customXml/_rels/item1.xml.rels -backend/output/test_validated_20260130_023857/hello/docProps/app.xml -backend/output/test_validated_20260130_023857/hello/docProps/core.xml -backend/output/test_validated_20260130_023857/hello/word/document.xml -backend/output/test_validated_20260130_023857/hello/word/fontTable.xml -backend/output/test_validated_20260130_023857/hello/word/settings.xml -backend/output/test_validated_20260130_023857/hello/word/styles.xml -backend/output/test_validated_20260130_023857/hello/word/webSettings.xml -backend/output/test_validated_20260130_023857/hello/word/_rels/document.xml.rels -backend/output/test_validated_20260130_023857/hello/word/theme/theme1.xml -backend/output/test_validated_20260130_023857/word/comments.xml -backend/output/test_validated_20260130_023857/word/document.xml -backend/output/test_validated_20260130_023857/word/endnotes.xml -backend/output/test_validated_20260130_023857/word/fontTable.xml -backend/output/test_validated_20260130_023857/word/footer1.xml -backend/output/test_validated_20260130_023857/word/footnotes.xml -backend/output/test_validated_20260130_023857/word/numbering.xml -backend/output/test_validated_20260130_023857/word/settings.xml -backend/output/test_validated_20260130_023857/word/styles.xml -backend/output/test_validated_20260130_023857/word/webSettings.xml -backend/output/test_validated_20260130_023857/word/_rels/document.xml.rels -backend/output/test_validated_20260130_023857/word/glossary/document.xml -backend/output/test_validated_20260130_023857/word/glossary/fontTable.xml -backend/output/test_validated_20260130_023857/word/glossary/settings.xml -backend/output/test_validated_20260130_023857/word/glossary/styles.xml -backend/output/test_validated_20260130_023857/word/glossary/webSettings.xml -backend/output/test_validated_20260130_023857/word/glossary/_rels/document.xml.rels -backend/output/test_validated_20260130_023857/word/media/image1.png -backend/output/test_validated_20260130_023857/word/media/image2.png -backend/output/test_validated_20260130_023857/word/media/image3.png -backend/output/test_validated_20260130_023857/word/theme/theme1.xml -backend/output/hello.docx -backend.Tests/Fixtures/test.docx -/backend.Tests/Fixtures -frontend/.vscode/extensions.json -frontend/.vscode/launch.json -frontend/.vscode/tasks.json -frontend/.vscode/extensions.json -frontend/.vscode/launch.json -frontend/.vscode/tasks.json -presentation/architecture.md -presentation/architecture.pdf -presentation/architecture.pptx +tests/backend.Tests/obj +src/backend/.idea +src/backend/bin +src/backend/obj +src/backend/output/ +src/frontend/.vscode/extensions.json +src/frontend/.vscode/launch.json +src/frontend/.vscode/tasks.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..bab7372 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# Thesis Validator + +A full-stack web app for validating `.docx` thesis documents against formatting, layout, structure, and grammar rules. + +## Demo + +Screenshots from the current app: + +![Main page](docs/screenshots/MainPage.png) + +![Rule settings](docs/screenshots/RuleSettings.png) + +![Validation results](docs/screenshots/ValidationResult.png) + +## Overview + +Thesis Validator helps students check Word thesis documents before submission. Users upload a `.docx` file, choose validation rules, review grouped issues, and can download an annotated copy with comments placed in the document. + +I built this project to practice a realistic full-stack workflow: document parsing, rule-based validation, API design, frontend state management, and automated backend testing. The app is useful for students who want a faster way to catch formatting and structure problems that are easy to miss during manual review. + +## Features + +- Upload and validate `.docx` thesis files from an Angular interface. +- Select validation rules by category before running a check. +- Validate formatting rules such as font family, single spaces, title punctuation, and text justification. +- Check layout and structure rules including paragraph spacing, indentation, heading depth, table of contents, empty sections, and figure captions. +- Run grammar checks through a local LanguageTool service. +- Skip front matter before the table of contents and skip Word text boxes when needed. +- View validation results grouped by category with error and warning counts. +- Download an annotated `.docx` file with comments marking detected issues. + +## Tech Stack + +- Frontend: Angular 18, TypeScript, RxJS, Tailwind CSS, lucide-angular +- Backend: ASP.NET Core 9, C#, Minimal APIs, Swagger/OpenAPI +- Database: None +- Auth: None +- Testing: xUnit, Moq, coverlet, Angular/Karma test setup +- Deployment: GitHub Actions CI for frontend build and backend build/tests +- Other: Docker Compose, LanguageTool, DocumentFormat.OpenXml, Postman collection + +## Getting Started + +### 1. Clone the repository + +```bash +git clone +cd thesis-validator +``` + +### 2. Install dependencies + +Install the frontend packages: + +```bash +cd src/frontend +npm ci +``` + +Restore the .NET solution from the repository root: + +```bash +cd ../.. +dotnet restore ThesisValidator.sln +``` + +### 3. Configure environment variables + +The default local configuration works for basic development. The backend reads its local settings from `src/backend/appsettings.json` and `src/backend/appsettings.Development.json`. + +Start the local grammar service before using grammar validation: + +```bash +docker compose -f docker/docker-compose.yml up -d +``` + +### 4. Run the development servers + +Start the backend API from the repository root: + +```bash +dotnet run --project src/backend/ThesisValidator.Api.csproj --launch-profile http +``` + +In a second terminal, start the Angular frontend: + +```bash +cd src/frontend +npm start +``` + +### 5. Open the app locally + +- Frontend: `http://localhost:4200` +- Backend API: `http://localhost:5213` +- Swagger UI: `http://localhost:5213/swagger` +- LanguageTool service: `http://localhost:8010` + +The Angular dev server proxies `/api` requests to `http://localhost:5213` through `src/frontend/proxy.conf.json`. + +## Environment Variables + +| Variable | Purpose | Required | +|---|---|---| +| `ASPNETCORE_ENVIRONMENT` | Sets the backend environment. The launch profile sets this to `Development` locally. | No | +| `LanguageTool__BaseUrl` | Overrides the LanguageTool service URL. Defaults to `http://localhost:8010`. | No | +| `Cors__AllowedOrigins__0` | Allows a frontend origin outside the default development origin. Defaults to `http://localhost:4200` in development. | No | + +No secrets are required for basic local setup. + +## Available Scripts + +Frontend commands from `src/frontend/package.json`: + +```bash +npm start +npm run build +npm run watch +npm test +``` + +Backend and solution commands: + +```bash +dotnet restore ThesisValidator.sln +dotnet build src/backend/ThesisValidator.Api.csproj +dotnet test tests/backend.Tests/ThesisValidator.Api.Tests.csproj +dotnet run --project src/backend/ThesisValidator.Api.csproj --launch-profile http +``` + +Local service command: + +```bash +docker compose -f docker/docker-compose.yml up -d +``` + +## Project Structure + +```text +. +|-- .github/workflows/ci.yml +|-- docker/docker-compose.yml +|-- docs/screenshots/ +|-- src/ +| |-- backend/ +| | |-- Annotation/ +| | |-- Application/Validation/ +| | |-- DocumentProcessing/ +| | |-- Endpoints/Documents/ +| | |-- Infrastructure/LanguageTool/ +| | |-- Rules/ +| | |-- Program.cs +| | `-- ThesisValidator.Api.csproj +| `-- frontend/ +| |-- src/app/components/ +| |-- src/app/models/ +| |-- src/app/services/ +| |-- proxy.conf.json +| `-- package.json +|-- tests/backend.Tests/ +|-- ThesisValidator.postman_collection.json +`-- ThesisValidator.sln +``` + +## Architecture / Implementation Notes + +The backend uses ASP.NET Core Minimal APIs. Document routes are grouped under `/api/documents` and include endpoints for validation, annotated downloads, available rules, and health checks. + +Validation is rule-based. Each rule implements the shared validation rule framework and is registered through dependency injection by scanning the backend assembly. Rules are grouped around formatting, layout, structure, and language checks. + +Document processing uses `DocumentFormat.OpenXml` to inspect Word documents, extract text, resolve formatting, detect headings, analyze lists, detect figure captions, and apply comments to annotated output files. + +Grammar validation calls a local LanguageTool service through an injected HTTP client. Docker Compose provides the service for local development. + +The Angular frontend uses standalone components, signals, computed state, and a `ValidationService` for API calls. The main app state moves between upload, validating, and results views. Validation results are normalized on the client before rendering. + +The repository includes backend tests with xUnit and fixture `.docx` files. GitHub Actions builds the Angular frontend and runs the backend build and tests on pushes and pull requests to `main`. + +## What I Learned + +- I learned how to design a rule-based validation pipeline that can grow without hard-coding every rule into the API layer. +- I practiced reading and annotating Word documents with OpenXML instead of treating uploaded files as plain text. +- I learned how to connect an Angular dev server to a local ASP.NET Core API through a proxy configuration. +- I practiced separating document parsing, validation rules, API responses, and frontend rendering into smaller pieces. +- I learned how to test document validation behavior with real `.docx` fixtures. + +## Future Improvements + +- Add more frontend unit tests around file upload, rule selection, and result rendering. +- Add end-to-end tests for the upload-to-annotated-download workflow. +- Improve accessibility for keyboard navigation, focus states, and screen reader labels. +- Add CI checks for frontend tests once the test suite is expanded. +- Add deployment configuration and replace the demo placeholder with a hosted link. +- Add clearer validation profiles for different university thesis requirements. +- Document how to add a new backend validation rule. + +## License + +> No license has been added yet. diff --git a/thesis-validator.sln b/ThesisValidator.sln similarity index 88% rename from thesis-validator.sln rename to ThesisValidator.sln index 71d617a..a13ed12 100644 --- a/thesis-validator.sln +++ b/ThesisValidator.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "backend", "backend\backend.csproj", "{B5CEE1AB-68C5-FCEC-EA61-653FFB609ED2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThesisValidator.Api", "src\backend\ThesisValidator.Api.csproj", "{B5CEE1AB-68C5-FCEC-EA61-653FFB609ED2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "backend.Tests", "backend.Tests\backend.Tests.csproj", "{871FB478-6040-4FAA-A744-1F98C040DB41}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThesisValidator.Api.Tests", "tests\backend.Tests\ThesisValidator.Api.Tests.csproj", "{871FB478-6040-4FAA-A744-1F98C040DB41}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/backend.Tests/Exploratory/FontExplorationTests.cs b/backend.Tests/Exploratory/FontExplorationTests.cs deleted file mode 100644 index 3943302..0000000 --- a/backend.Tests/Exploratory/FontExplorationTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using backend.Models; -using backend.Tests.Helpers; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Wordprocessing; -using Xunit.Abstractions; - -namespace backend.Tests.Exploratory; - -public class FontExplorationTests -{ - private readonly ITestOutputHelper _output; - public FontExplorationTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void Explore_Fonts() - { - using var doc = DocxTestHelper.OpenDocxAsRead("Fonts/fonts.docx"); - var body = doc.MainDocumentPart.Document.Body; - - foreach (var paragraph in body.Elements()) - { - Console.WriteLine("paragraph:", paragraph); - foreach (var text in paragraph.Descendants()) - { - _output.WriteLine(text.Text); - } - } - } - - [Fact] - public void Print_text_with_font_info() - { - using var doc = DocxTestHelper.OpenDocxAsRead("test.docx"); - - var errros = ValidateTimesNewRoman(doc).ToList(); - foreach (var error in errros) - { - _output.WriteLine(error.Message); - } - _output.WriteLine($"Done Errors Count: {errros.Count.ToString()}"); - - } - public IEnumerable ValidateTimesNewRoman( - WordprocessingDocument doc) - { - var body = doc.MainDocumentPart!.Document.Body!; - var errors = new List(); - - int paragraphIndex = 0; - - foreach (var paragraph in body.Elements()) - { - paragraphIndex++; - - foreach (var run in paragraph.Elements()) - { - var text = string.Concat( - run.Elements().Select(t => t.Text)); - - if (string.IsNullOrWhiteSpace(text)) - continue; - - var font = ResolveEffectiveFont(doc, paragraph, run); - - if (!string.Equals(font, "Times New Roman", - StringComparison.OrdinalIgnoreCase)) - { - errors.Add(new ValidationResult - { - IsError = true, - Message = - $"Invalid font '{font}' in paragraph {paragraphIndex} text: {text}", - }); - } - } - } - - return errors; - } - string? ResolveEffectiveFont( - WordprocessingDocument doc, - Paragraph paragraph, - Run run) - { - // 1. Run-level font - var runFont = run - .RunProperties? - .RunFonts? - .Ascii; - - if (!string.IsNullOrEmpty(runFont)) - return runFont; - - // 2. Paragraph style font - var paraFont = GetParagraphStyleFont(doc, paragraph); - if (!string.IsNullOrEmpty(paraFont)) - return paraFont; - - // 3. Default document font - return GetDefaultFont(doc); - } - - string? GetParagraphStyleFont( - WordprocessingDocument doc, - Paragraph paragraph) - { - var styleId = paragraph - .ParagraphProperties? - .ParagraphStyleId? - .Val; - - if (styleId == null) - return null; - - var styles = doc.MainDocumentPart?.StyleDefinitionsPart?.Styles; - - var style = styles? - .Elements