diff --git a/.agents/skills/implement/SKILL.md b/.agents/skills/implement/SKILL.md new file mode 100644 index 000000000..0402e338c --- /dev/null +++ b/.agents/skills/implement/SKILL.md @@ -0,0 +1,197 @@ +--- +name: implement +description: "End-to-end implementation workflow: plan, implement, verify, commit, and open a draft PR. Use when asked to implement, fix, build, or work on something." +--- + +# Implement Workflow + +This skill handles the full lifecycle: plan -> implement -> verify -> commit -> draft PR. + +## Phase 1: Plan (requires approval) + +1. **Understand the request** — Read the issue description, linked context, or user instructions. +2. **Explore the codebase** — Find relevant files, understand existing patterns, read tests. +3. **Create an implementation plan**: + - What files will be modified/created + - What the changes will do + - What edge cases to consider + - What tests to add or update +4. **Present the plan and STOP** — Wait for explicit user approval before proceeding. + +Do NOT start coding until the plan is approved. If requirements are ambiguous, ask. + +## Phase 2: Implement + +Execute the approved plan: +- Follow existing code patterns and conventions in the repo +- Keep changes minimal and focused — don't refactor unrelated code +- Kotlin warnings are treated as errors (`allWarningsAsErrors = true`) — write clean code +- Use `org.wordpress.aztec` package conventions +- **Unit tests**: Add or update tests covering new/changed logic. Follow existing test patterns in the repo. If writing meaningful tests would require disproportionate effort (e.g., complex setup, heavy mocking of framework internals), skip but notify the developer explaining why. + +## Phase 3: Verify + +Run verification checks and fix any failures. Iterate until all checks pass. + +### 3a: Compile + +```bash +./gradlew :aztec:assembleRelease :app:assembleDebug +``` + +If other modules were changed, compile those too: +```bash +./gradlew assembleRelease +``` + +### 3b: Lint + +```bash +./gradlew ktlint +``` + +- Fix violations — prefer real fixes over suppression +- Only suppress when it's a false positive and document why + +### 3c: Unit Tests + +```bash +./gradlew aztec:testRelease +``` + +Or run specific tests related to the changes: +```bash +./gradlew :aztec:testReleaseUnitTest --tests "org.wordpress.aztec.SpecificTest" --info +``` + +**Test rules:** +- NEVER weaken or remove assertions to make tests pass +- NEVER modify production code just to pass a test without permission +- Tests that pass only by not crashing are invalid — every test needs meaningful assertions +- If a test won't pass after reasonable attempts: stop and ask + +### 3d: Fix Errors + +If any step fails: +1. Analyze the error +2. Fix the issue +3. Re-run the failing check +4. Repeat until green + +## Phase 4: Present Changes (requires approval) + +Show a summary of all changes made: +- Files modified/created +- Key behavioral changes +- Test coverage + +**STOP and wait for user approval** before committing. + +## Phase 5: Commit and Draft PR + +Only proceed after explicit approval. + +### 5a: Run Final Lint + +```bash +./gradlew ktlint +``` + +Fix any remaining issues. + +### 5b: Inspect Changes + +```bash +git status +git diff --stat +git diff +``` + +### 5c: Plan Commits + +Review the changes and determine if they should be split into multiple commits: +- Independent logical units = separate commits +- Bug fix + feature = separate commits +- Formatting + logic = separate commits + +For **each commit**, prepare: +1. **Commit message**: Imperative summary + brief body +2. **Files**: Paths to stage +3. **Summary**: What and why + +**Commit message format** — use direct multi-line strings: +```bash +git commit -m "Imperative summary + +- Detail one +- Detail two +" +``` + +**Rules:** +- NO co-author lines — NEVER add "Co-Authored-By" or AI attribution +- Each commit should be cohesive and buildable +- Use `git add -p` to split mixed concerns if needed + +### 5d: Stage and Commit + +```bash +git add +git commit -m "message" +``` + +### 5e: Push and Create Draft PR + +```bash +# Get the correct remote owner/repo +git remote get-url origin + +# Push +git push -u origin HEAD + +# Create draft PR +PAGER=cat gh pr create --draft --title "PR title" --body "$(cat <<'EOF' +### Fix + + +### Test +1. Step 1 +2. Step 2 +EOF +)" +``` + +**PR rules:** +- Title: short, under 70 characters +- Body: follows the repo's PR template format (### Fix, ### Test, ### Review) +- Always create as **draft** +- Return the PR URL when done + +## Handling Edge Cases + +### Working on an issue with a branch name +If the user mentions an issue with a known branch name: +```bash +git checkout trunk && git pull && git checkout -b +``` + +### Determining diff base for existing PRs +PRs can be stacked — check the actual base: +```bash +PAGER=cat gh pr view --json baseRefName +``` + +### Posting review comments +When reviewing or addressing PR feedback: +```bash +# Create review JSON +printf '%s\n' '{ + "event": "COMMENT", + "body": "Review comment", + "comments": [ + {"path": "file.kt", "line": 42, "body": "Inline comment"} + ] +}' > /tmp/pr_review.json + +PAGER=cat gh api repos/{owner}/{repo}/pulls/{number}/reviews --method POST --input /tmp/pr_review.json +``` diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 000000000..2b7a412b8 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..79a19a6df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,139 @@ +# Instructions + +This file provides guidance to coding agents when working with code in this repository. + +## General Approach + +- Analyze assumptions and provide counterpoints — prioritize truth over agreement. +- Treat me as an expert Android developer. Give overviews, not tutorials. +- Always give an overview of the solution before diving into implementation (unless explicitly asked to implement right away). +- Do not add comments for trivial logic or when the name is descriptive enough. + +## Architecture Overview + +**AztecEditor-Android** is a rich-text HTML editor library for Android, built on the `EditText` / `Spannable` API. + +**Stack**: Kotlin, Android Spannable API, Plugin architecture, Gradle with Kotlin DSL + +### Module Structure + +``` +:app — Demo application +:aztec — Core editor library (main deliverable) +:glide-loader — Image loading via Glide +:picasso-loader — Image loading via Picasso +:wordpress-comments — WordPress comments plugin +:wordpress-shortcodes — WordPress shortcodes plugin +:media-placeholders — Media placeholders with Compose support +``` + +All plugin/loader modules depend on `:aztec`. Published independently to Automattic S3 Maven (`org.wordpress:aztec`, `org.wordpress.aztec:*`). + +### Code Navigation (package: `org.wordpress.aztec`) + +| To find... | Look in... | +|---------------------------------|------------------------------------------------------| +| Main editor component | `aztec/.../AztecText.kt` (extends EditText, ~3K LOC) | +| Editor facade / initialization | `aztec/.../Aztec.kt` | +| HTML parsing | `aztec/.../AztecParser.kt`, `AztecTagHandler.kt` | +| Custom spans (visual rendering) | `aztec/.../spans/` (62 files — one per HTML element) | +| Text formatting logic | `aztec/.../formatting/` (Block, Inline, Line, List) | +| Block element handlers | `aztec/.../handlers/` (Heading, List, Quote, etc.) | +| Plugin interfaces | `aztec/.../plugins/` (IAztecPlugin, IToolbarButton) | +| HTML source editor | `aztec/.../source/SourceViewEditText.kt` | +| Text change watchers | `aztec/.../watchers/` (30+ files, API-version buckets)| +| Toolbar | `aztec/.../toolbar/AztecToolbar.kt` | +| Undo/redo | `aztec/.../History.kt` | +| Compose placeholders | `media-placeholders/.../ComposePlaceholder*.kt` | + +### Key Architectural Patterns + +1. **Span-based rendering** — Each HTML element has a custom `Span` class. Spans carry both visual rendering and semantic meaning. The `Spannable` API is central to everything. + +2. **Plugin architecture** — `IAztecPlugin` with sub-interfaces (`IToolbarButton`, `IClipboardPastePlugin`, `IOnDrawPlugin`). Plugins for `html2visual` and `visual2html` conversion pipelines. + +3. **Facade pattern** — `Aztec` class provides a builder-like fluent API to wire up editor, toolbar, source view, and plugins. + +4. **Handler/Formatter split** — Handlers deal with block element structure (headings, lists, quotes). Formatters apply styling (inline, block, line-block, list, link, indent). + +5. **Bidirectional HTML conversion** — HTML string <-> Spannable representation, with plugin hooks at each stage. + +6. **Watcher pattern** — Multiple `TextWatcher` implementations with API-version-specific buckets (API 25, 26+) for Samsung/OEM compatibility. + +### Common Crash Patterns (from recent PRs) + +- `IndexOutOfBoundsException` from span start/end being out of bounds — always clamp to `[0, text.length]` +- `IllegalArgumentException` when span end < span start — validate before applying +- Samsung/custom emoji OEM issues causing unexpected span states +- Thread safety in `AztecAttributes` — operations should be synchronized + +## Git Operations (CRITICAL) +- **NEVER commit without explicit permission** +- **NEVER push without explicit permission** +- **NEVER create a PR without explicit permission** +- When asked to "fix" or "update" something, that does NOT imply permission to commit/push +- Always wait for explicit "commit", "push", or "create PR" commands + +## Build Commands + +```bash +# Build and run the demo app +./gradlew :app:installDebug && adb shell am start -n org.wordpress.aztec/org.wordpress.aztec.demo.MainActivity + +# Build the demo app (without installing) +./gradlew :app:assembleDebug + +# Build the library +./gradlew :aztec:assembleRelease + +# Lint (ktlint + Android lint) +./gradlew ktlint +./gradlew lintRelease + +# Unit tests (core library) +./gradlew aztec:testRelease + +# All unit tests +./gradlew testRelease + +# Specific test class +./gradlew :aztec:testReleaseUnitTest --tests "org.wordpress.aztec.AztecParserTest" --info + +# Specific test method +./gradlew :aztec:testReleaseUnitTest --tests "org.wordpress.aztec.AztecParserTest.testMethodName" --info +``` + +### Build Configuration + +- All Kotlin warnings are errors (`allWarningsAsErrors = true`) +- Versions are defined in root `build.gradle` — check there for current Kotlin, AGP, SDK, and dependency versions + +## GitHub Commands + +```bash +PAGER=cat gh pr list +PAGER=cat gh pr view +PAGER=cat gh pr view --comments +PAGER=cat gh pr diff +``` + +## PR Template + +PRs follow this format (from `.github/PULL_REQUEST_TEMPLATE.md`): +``` +### Fix + + +### Test +1. Step 1 +2. Step 2 + +### Review +@reviewer +``` + +## Skills + +| Skill | Trigger phrases | +|-------------|------------------------------------------------------------------------------------------------------| +| `implement` | "implement", "fix this", "work on this", "build this", "add feature", any implementation request | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt b/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt index 101dbf76d..683dde569 100644 --- a/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt +++ b/app/src/main/kotlin/org/wordpress/aztec/demo/MainActivity.kt @@ -475,6 +475,7 @@ open class MainActivity : AppCompatActivity(), ToolbarAction.LINK, ToolbarAction.UNDERLINE, ToolbarAction.STRIKETHROUGH, + ToolbarAction.REDACTED, ToolbarAction.ALIGN_LEFT, ToolbarAction.ALIGN_CENTER, ToolbarAction.ALIGN_RIGHT, diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt index 833d1d542..3139aa869 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTagHandler.kt @@ -31,6 +31,7 @@ import org.wordpress.aztec.plugins.html2visual.IHtmlTagHandler import org.wordpress.aztec.source.CssStyleFormatter import org.wordpress.aztec.spans.AztecAudioSpan import org.wordpress.aztec.spans.AztecBackgroundColorSpan +import org.wordpress.aztec.spans.AztecRedactedSpan import org.wordpress.aztec.spans.AztecHorizontalRuleSpan import org.wordpress.aztec.spans.AztecImageSpan import org.wordpress.aztec.spans.AztecListItemSpan @@ -176,6 +177,10 @@ class AztecTagHandler(val context: Context, val plugins: List = Ar private fun handleBackgroundColorSpanTag(attributes: Attributes, tag: String, nestingLevel: Int): IAztecSpan { val attrs = AztecAttributes(attributes) + val classAttr = attrs.getValue("class") ?: "" + if (classAttr.contains("redacted") || (tagStack.isNotEmpty() && tagStack.last() is AztecRedactedSpan)) { + return AztecRedactedSpan(attrs) + } return if (CssStyleFormatter.containsStyleAttribute( attrs, CssStyleFormatter.CSS_BACKGROUND_COLOR_ATTRIBUTE diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt index b33cc9b3e..8afc2e276 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt @@ -1441,6 +1441,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown AztecTextFormat.FORMAT_CITE, AztecTextFormat.FORMAT_UNDERLINE, AztecTextFormat.FORMAT_STRIKETHROUGH, + AztecTextFormat.FORMAT_REDACTED, AztecTextFormat.FORMAT_BACKGROUND, AztecTextFormat.FORMAT_HIGHLIGHT, AztecTextFormat.FORMAT_CODE -> inlineFormatter.toggle(textFormat) @@ -1483,6 +1484,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown AztecTextFormat.FORMAT_CITE, AztecTextFormat.FORMAT_UNDERLINE, AztecTextFormat.FORMAT_STRIKETHROUGH, + AztecTextFormat.FORMAT_REDACTED, AztecTextFormat.FORMAT_BACKGROUND, AztecTextFormat.FORMAT_MARK, AztecTextFormat.FORMAT_HIGHLIGHT, diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTextFormat.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTextFormat.kt index 37a9bf69e..e61e60e12 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecTextFormat.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecTextFormat.kt @@ -38,5 +38,6 @@ enum class AztecTextFormat : ITextFormat { FORMAT_CODE, FORMAT_BACKGROUND, FORMAT_MARK, - FORMAT_HIGHLIGHT + FORMAT_HIGHLIGHT, + FORMAT_REDACTED } diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/formatting/InlineFormatter.kt b/aztec/src/main/kotlin/org/wordpress/aztec/formatting/InlineFormatter.kt index f72e99a13..90bad77ea 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/formatting/InlineFormatter.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/formatting/InlineFormatter.kt @@ -27,6 +27,7 @@ import org.wordpress.aztec.spans.AztecUnderlineSpan import org.wordpress.aztec.spans.HighlightSpan import org.wordpress.aztec.spans.IAztecExclusiveInlineSpan import org.wordpress.aztec.spans.IAztecInlineSpan +import org.wordpress.aztec.spans.AztecRedactedSpan import org.wordpress.aztec.spans.MarkSpan import org.wordpress.aztec.watchers.TextChangedEvent @@ -99,6 +100,7 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle, private val h AztecTextFormat.FORMAT_EMPHASIS, AztecTextFormat.FORMAT_CITE, AztecTextFormat.FORMAT_STRIKETHROUGH, + AztecTextFormat.FORMAT_REDACTED, AztecTextFormat.FORMAT_BACKGROUND, AztecTextFormat.FORMAT_UNDERLINE, AztecTextFormat.FORMAT_CODE -> { @@ -321,6 +323,7 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle, private val h AztecStyleEmphasisSpan::class.java -> AztecTextFormat.FORMAT_EMPHASIS AztecStyleCiteSpan::class.java -> AztecTextFormat.FORMAT_CITE AztecStrikethroughSpan::class.java -> AztecTextFormat.FORMAT_STRIKETHROUGH + AztecRedactedSpan::class.java -> AztecTextFormat.FORMAT_REDACTED AztecUnderlineSpan::class.java -> AztecTextFormat.FORMAT_UNDERLINE AztecCodeSpan::class.java -> AztecTextFormat.FORMAT_CODE AztecBackgroundColorSpan::class.java -> return AztecTextFormat.FORMAT_BACKGROUND @@ -475,6 +478,7 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle, private val h AztecTextFormat.FORMAT_EMPHASIS -> AztecStyleEmphasisSpan() AztecTextFormat.FORMAT_CITE -> AztecStyleCiteSpan() AztecTextFormat.FORMAT_STRIKETHROUGH -> AztecStrikethroughSpan() + AztecTextFormat.FORMAT_REDACTED -> AztecRedactedSpan() AztecTextFormat.FORMAT_UNDERLINE -> AztecUnderlineSpan() AztecTextFormat.FORMAT_CODE -> AztecCodeSpan(codeStyle) AztecTextFormat.FORMAT_BACKGROUND -> AztecBackgroundColorSpan(backgroundSpanColor ?: R.color.background) diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecRedactedSpan.kt b/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecRedactedSpan.kt new file mode 100644 index 000000000..07671e4c4 --- /dev/null +++ b/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecRedactedSpan.kt @@ -0,0 +1,22 @@ +package org.wordpress.aztec.spans + +import android.graphics.Color +import android.text.TextPaint +import android.text.style.CharacterStyle +import org.wordpress.aztec.AztecAttributes + +class AztecRedactedSpan( + override var attributes: AztecAttributes = AztecAttributes() +) : CharacterStyle(), IAztecInlineSpan { + + override val TAG = "span" + + override fun updateDrawState(tp: TextPaint) { + tp.bgColor = Color.RED + tp.isStrikeThruText = true + } + + init { + attributes.setValue("class", "redacted") + } +} diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarAction.kt b/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarAction.kt index 4782be8d8..47193ff4f 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarAction.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarAction.kt @@ -97,6 +97,12 @@ enum class ToolbarAction constructor( ToolbarActionType.INLINE_STYLE, setOf(AztecTextFormat.FORMAT_STRIKETHROUGH), R.layout.format_bar_button_strikethrough), + REDACTED( + R.id.format_bar_button_redacted, + R.drawable.format_bar_button_redacted_selector, + ToolbarActionType.INLINE_STYLE, + setOf(AztecTextFormat.FORMAT_REDACTED), + R.layout.format_bar_button_redacted), ALIGN_LEFT(R.id.format_bar_button_align_left, R.drawable.format_bar_button_align_left_selector, ToolbarActionType.BLOCK_STYLE, diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarItems.kt b/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarItems.kt index 37e0acef5..35190fe0a 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarItems.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/toolbar/ToolbarItems.kt @@ -60,6 +60,7 @@ sealed class ToolbarItems { ToolbarAction.LINK, ToolbarAction.UNDERLINE, ToolbarAction.STRIKETHROUGH, + ToolbarAction.REDACTED, ToolbarAction.ALIGN_LEFT, ToolbarAction.ALIGN_CENTER, ToolbarAction.ALIGN_RIGHT, @@ -76,6 +77,7 @@ sealed class ToolbarItems { ToolbarAction.LINK, ToolbarAction.UNDERLINE, ToolbarAction.STRIKETHROUGH, + ToolbarAction.REDACTED, ToolbarAction.ALIGN_LEFT, ToolbarAction.ALIGN_CENTER, ToolbarAction.ALIGN_RIGHT, diff --git a/aztec/src/main/res/drawable/format_bar_button_redacted.xml b/aztec/src/main/res/drawable/format_bar_button_redacted.xml new file mode 100644 index 000000000..2730d0c3c --- /dev/null +++ b/aztec/src/main/res/drawable/format_bar_button_redacted.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/aztec/src/main/res/drawable/format_bar_button_redacted_disabled.xml b/aztec/src/main/res/drawable/format_bar_button_redacted_disabled.xml new file mode 100644 index 000000000..f0d78ff69 --- /dev/null +++ b/aztec/src/main/res/drawable/format_bar_button_redacted_disabled.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/aztec/src/main/res/drawable/format_bar_button_redacted_highlighted.xml b/aztec/src/main/res/drawable/format_bar_button_redacted_highlighted.xml new file mode 100644 index 000000000..57206772b --- /dev/null +++ b/aztec/src/main/res/drawable/format_bar_button_redacted_highlighted.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/aztec/src/main/res/drawable/format_bar_button_redacted_selector.xml b/aztec/src/main/res/drawable/format_bar_button_redacted_selector.xml new file mode 100644 index 000000000..b630d446b --- /dev/null +++ b/aztec/src/main/res/drawable/format_bar_button_redacted_selector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/aztec/src/main/res/layout/format_bar_button_redacted.xml b/aztec/src/main/res/layout/format_bar_button_redacted.xml new file mode 100644 index 000000000..0b49b942d --- /dev/null +++ b/aztec/src/main/res/layout/format_bar_button_redacted.xml @@ -0,0 +1,7 @@ + + diff --git a/aztec/src/main/res/values/strings.xml b/aztec/src/main/res/values/strings.xml index bdb615c9e..0b985cdc2 100644 --- a/aztec/src/main/res/values/strings.xml +++ b/aztec/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Italic Underline Strikethrough + Redacted Align Left Align Right Align Center