diff --git a/.github/workflows/developer-guide-docs.yml b/.github/workflows/developer-guide-docs.yml index 277552a037..79f8164049 100644 --- a/.github/workflows/developer-guide-docs.yml +++ b/.github/workflows/developer-guide-docs.yml @@ -270,6 +270,52 @@ jobs: echo "Vale exited with status $STATUS and reported $ISSUE_COUNT issue(s). The final quality-gate step will fail the build." >&2 fi + - name: Run paragraph capitalization check + run: | + set -euo pipefail + REPORT_DIR="build/developer-guide/reports" + REPORT_FILE="${REPORT_DIR}/paragraph-capitalization-report.json" + mkdir -p "$REPORT_DIR" + set +e + ruby scripts/developer-guide/check_paragraph_capitalization.rb \ + --output "$REPORT_FILE" \ + docs/developer-guide/developer-guide.asciidoc + STATUS=$? + set -e + echo "PARAGRAPH_CAP_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" + echo "PARAGRAPH_CAP_STATUS=$STATUS" >> "$GITHUB_ENV" + if [ "$STATUS" -ne 0 ]; then + echo "Paragraph capitalization check failed. The final quality-gate step will fail the build." >&2 + fi + + - name: Set up Java 17 for LanguageTool + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install language-tool-python + run: | + set -euo pipefail + pip install --user language-tool-python==2.9.4 + + - name: Run LanguageTool grammar check (advisory) + run: | + set -euo pipefail + REPORT_DIR="build/developer-guide/reports" + REPORT_FILE="${REPORT_DIR}/languagetool-report.json" + mkdir -p "$REPORT_DIR" + set +e + python3 scripts/developer-guide/run_languagetool.py \ + --html build/developer-guide/html/developer-guide.html \ + --output "$REPORT_FILE" + STATUS=$? + set -e + LT_COUNT="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("total",0))' "$REPORT_FILE" 2>/dev/null || echo 0)" + echo "LANGUAGETOOL_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" + echo "LANGUAGETOOL_COUNT=$LT_COUNT" >> "$GITHUB_ENV" + echo "LanguageTool flagged ${LT_COUNT} match(es). This is advisory and does not fail the build." >&2 + - name: Check for unused developer guide images run: | set -euo pipefail @@ -316,6 +362,23 @@ jobs: --details-key details \ --preview-limit 10 + - name: Summarize paragraph capitalization findings + id: summarize_paragraph_cap + if: always() + run: | + python3 scripts/developer-guide/summarize_reports.py paragraph-capitalization \ + --report "${PARAGRAPH_CAP_REPORT}" \ + --status "${PARAGRAPH_CAP_STATUS:-0}" \ + --output "${GITHUB_OUTPUT}" + + - name: Summarize LanguageTool findings + id: summarize_languagetool + if: always() + run: | + python3 scripts/developer-guide/summarize_reports.py languagetool \ + --report "${LANGUAGETOOL_REPORT}" \ + --output "${GITHUB_OUTPUT}" + - name: Upload HTML artifact if: always() uses: actions/upload-artifact@v4 @@ -360,6 +423,22 @@ jobs: ${{ env.UNUSED_IMAGES_TEXT }} if-no-files-found: warn + - name: Upload paragraph capitalization report + if: always() + uses: actions/upload-artifact@v4 + with: + name: developer-guide-paragraph-capitalization + path: ${{ env.PARAGRAPH_CAP_REPORT }} + if-no-files-found: warn + + - name: Upload LanguageTool report + if: always() + uses: actions/upload-artifact@v4 + with: + name: developer-guide-languagetool + path: ${{ env.LANGUAGETOOL_REPORT }} + if-no-files-found: warn + - name: Fail build on developer guide quality issues if: always() run: | @@ -379,6 +458,11 @@ jobs: echo "Either delete the orphan image or add an image:: reference for it from a developer-guide asciidoc file." >&2 FAIL=1 fi + if [ "${PARAGRAPH_CAP_STATUS:-0}" != "0" ]; then + echo "Paragraph capitalization check reported findings (exit status ${PARAGRAPH_CAP_STATUS}). See the developer-guide-paragraph-capitalization artifact." >&2 + echo "Rewrite the flagged paragraph so its first prose word starts with a capital letter." >&2 + FAIL=1 + fi if [ "$FAIL" -ne 0 ]; then echo "Developer guide quality gates failed. Warnings from vale and asciidoctor are treated as build-breaking errors." >&2 exit 1 @@ -393,6 +477,8 @@ jobs: VALE_SUMMARY: ${{ steps.summarize_vale.outputs.summary }} UNUSED_SUMMARY: ${{ steps.summarize_unused_images.outputs.summary }} UNUSED_DETAILS: ${{ steps.summarize_unused_images.outputs.details }} + PARAGRAPH_CAP_SUMMARY: ${{ steps.summarize_paragraph_cap.outputs.summary }} + LANGUAGETOOL_SUMMARY: ${{ steps.summarize_languagetool.outputs.summary }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -448,6 +534,12 @@ jobs: if (artifactLinks.has('developer-guide-unused-images')) { links.push(`- [Unused image report](${artifactLinks.get('developer-guide-unused-images')})`); } + if (artifactLinks.has('developer-guide-paragraph-capitalization')) { + links.push(`- [Paragraph capitalization report](${artifactLinks.get('developer-guide-paragraph-capitalization')})`); + } + if (artifactLinks.has('developer-guide-languagetool')) { + links.push(`- [LanguageTool report (advisory)](${artifactLinks.get('developer-guide-languagetool')})`); + } if (!links.length) { console.log('No artifacts found to report.'); @@ -458,9 +550,13 @@ jobs: const asciiSummary = process.env.ASCII_SUMMARY?.trim(); const valeSummary = process.env.VALE_SUMMARY?.trim(); const unusedSummary = process.env.UNUSED_SUMMARY?.trim(); + const paragraphCapSummary = process.env.PARAGRAPH_CAP_SUMMARY?.trim(); + const languagetoolSummary = process.env.LANGUAGETOOL_SUMMARY?.trim(); const asciiLink = artifactLinks.get('developer-guide-asciidoc-lint'); const valeLink = artifactLinks.get('developer-guide-vale-report'); const unusedLink = artifactLinks.get('developer-guide-unused-images'); + const paragraphCapLink = artifactLinks.get('developer-guide-paragraph-capitalization'); + const languagetoolLink = artifactLinks.get('developer-guide-languagetool'); if (asciiSummary) { qualityLines.push(`- AsciiDoc linter: ${asciiSummary}${asciiLink ? ` ([report](${asciiLink}))` : ''}`); @@ -468,6 +564,12 @@ jobs: if (valeSummary) { qualityLines.push(`- Vale: ${valeSummary}${valeLink ? ` ([report](${valeLink}))` : ''}`); } + if (paragraphCapSummary) { + qualityLines.push(`- Paragraph capitalization: ${paragraphCapSummary}${paragraphCapLink ? ` ([report](${paragraphCapLink}))` : ''}`); + } + if (languagetoolSummary) { + qualityLines.push(`- LanguageTool (advisory): ${languagetoolSummary}${languagetoolLink ? ` ([report](${languagetoolLink}))` : ''}`); + } if (unusedSummary) { qualityLines.push(`- Image references: ${unusedSummary}${unusedLink ? ` ([report](${unusedLink}))` : ''}`); } diff --git a/docs/developer-guide/.vale.ini b/docs/developer-guide/.vale.ini index 9b41db9245..350e0d1307 100644 --- a/docs/developer-guide/.vale.ini +++ b/docs/developer-guide/.vale.ini @@ -68,6 +68,16 @@ Packages = https://github.com/errata-ai/packages/releases/download/v0.2.0/Micros # does not equal "the same" (it allows different representations), etc. # The rule's substitutions damage precision. # +# write-good.ThereIs +# Flags any sentence that starts with "There is/are/was/were". The rule's +# premise — that sentences are stronger led by the subject — is reasonable +# in marketing copy but routinely wrong for technical reference. Existential +# "There are" is the natural way to introduce a count or set +# ("There are two ways to animate..."), and the rule's recommended rewrite +# ("Two ways to animate exist...") is awkward and harder to read. This rule +# also blocked the documented fix to a previous PR-5000-class bug, which is +# the situation our new paragraph-capitalization check is meant to catch. +# # Microsoft.GeneralURL # Suggests "address" instead of "URL" "for a general audience". Our audience # is developers wiring up HTTP requests; "URL" is the term every other API @@ -173,6 +183,7 @@ Microsoft.Passive = NO write-good.E-Prime = NO write-good.Weasel = NO write-good.TooWordy = NO +write-good.ThereIs = NO Microsoft.GeneralURL = NO Microsoft.HeadingAcronyms = NO Microsoft.Terms = NO diff --git a/docs/developer-guide/Advanced-Theming.asciidoc b/docs/developer-guide/Advanced-Theming.asciidoc index 11986fa6ab..6a2815b6bc 100644 --- a/docs/developer-guide/Advanced-Theming.asciidoc +++ b/docs/developer-guide/Advanced-Theming.asciidoc @@ -751,7 +751,7 @@ The type is omitted for the default unselected type, and may be one of _sel_ (se * `backgroundType` - a `Byte` object containing one of the constants for the background type defined in https://www.codenameone.com/javadoc/com/codename1/ui/plaf/Style.html[Style] under BACKGROUND_*. * `backgroundGradient` - contains an `Object` array containing 2 integers for the colors of the gradient. If the gradient is radial it contains 3 floating points defining the x, y & size of the gradient. -to set the foreground color of a selected button to red, a theme will define a property like: +To set the foreground color of a selected button to red, a theme will define a property like: `Button.sel#fgColor=ff0000` @@ -1118,7 +1118,7 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/advancedth ==== Styling the UI -the code above is most of the work but you still need to put everything together using the theme. This is what you've so far: +The code above is most of the work but you still need to put everything together using the theme. This is what you've so far: .Before applying the changes to the theme this is what you've image::img/psd2app-image15.png[Before applying the changes to the theme this is what you've,scaledwidth=20%] diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index ddb9777463..2bf0a074b2 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -578,7 +578,7 @@ Notice that denying this second request won't trigger another Codename One promp ===== Code changes -no explicit code changes needed for this functionality to "work." The respective API's will work like they always worked and will prompt the user seamlessly for permissions. +No explicit code changes needed for this functionality to "work." The respective API's will work like they always worked and will prompt the user seamlessly for permissions. TIP: Some behaviors that never occurred on Android but were legal in the past might start occurring with the switch to the new API. For example: the location manager might be null and your app must always be ready to deal with such a situation @@ -1171,7 +1171,7 @@ Permissions in Codename One are seamless. Codename One traverses the bytecode an For example, when accessing native functionality this won’t work since native code might require specialized permissions and you don’t/can’t run any serious analysis on it (it can be about anything). -if you require more permissions in your Android native code you need to define them in the build arguments using +If you require more permissions in your Android native code you need to define them in the build arguments using `android.permission.=true` for each permission you want to include. A full list of permissions are listed in Android's https://developer.android.com/reference/android/Manifest.permission.html[Manifest.permission documentation]. For example: @@ -1322,7 +1322,7 @@ The native sample in stack overflow bound the listener in the activity but here include::../demos/common/src/main/java/com/codename1/sms/intercept/NativeSMSInterceptor.java[tag=nativeSmsInterceptor,indent=0] ---- -that's easy! +That's easy! Notice that `isSupported()` returns false for all other OS's so you won't need to ask whether this is "Android" you can use `isSupported()`. @@ -1354,7 +1354,7 @@ include::../demos/common/src/main/java/com/mycompany/NativeCallback.java[tag=nat ---- -if you want to call it from Android or all the Java based platforms you can write this in the "native" code: +If you want to call it from Android or all the Java based platforms you can write this in the "native" code: [source,java] ---- @@ -1462,7 +1462,7 @@ For example, the Google Maps project includes the static callback method: include::../demos/common/src/main/java/com/codename1/googlemaps/MapContainerCallbacks.java[tag=fireMapChangeEvent,indent=0] ---- -defined in the `com.codename1.googlemaps.MapContainer` class. +It's defined in the `com.codename1.googlemaps.MapContainer` class. This method is called from Javascript inside a native interface using the following code: @@ -1533,7 +1533,7 @@ Jars don't include support for writing native code; you could use JNI in jars bu Jars don't support "proper" code completion, a common developer trick is to stick source code into the jar but that prevents usage with proprietary code. Cn1libs provide full IDE code completion (with JavaDoc hints) without exposing the sources. -two use cases for wanting JAR's and they both have different solutions: +There are two use cases for wanting JAR's and they both have different solutions: . Modularity . Working with an existing JARs @@ -1597,7 +1597,7 @@ The best way to discover the right syntax for such build hints is to set them th The obvious question is why do you need two files? -two types of build hints: required and appended. +There are two types of build hints: required and appended. Required build hints can be something like `ios.objC=true` which you want to always work. For example: if a cn1lib defines `ios.objC=true` and another cn1lib defines `ios.objC=false` @@ -1619,7 +1619,7 @@ for the developer to investigate issues in this process. `codenameone_settings.properties` file. If you need to change a flag later on you might need to alert users to make changes to their properties essentially negating the value of this feature... + be careful when adding properties here. -it's your responsibility as a library developer to decide which build hint goes into which file! + +It's your responsibility as a library developer to decide which build hint goes into which file! + Codename One can't automate this process as the whole process of build hints is by definition an ad hoc process. The rule of thumb is that a build hint with a numeric or boolean value is always a required property. If an entry has a string that you can append with another string then its probably an appended entry. @@ -2084,7 +2084,7 @@ iOS is practically identical to Android with some small caveats, iOS's equivalen You can inject more data into the plist by using the `ios.plistInject` build hint. -the equivalent in the iOS side would be +The equivalent on the iOS side would be: [source,xml] ---- @@ -2239,7 +2239,7 @@ image::img/5fe88406-61e4-11e5-951e-e09bd28a93c9.png[Freshdesk API Integration,sc ==== Step 4: Implement the public API and native interface -you've already looked at the final product of the public API in the previous step. Back up and walk through the process step-by-step. +You've already looked at the final product of the public API in the previous step. Back up and walk through the process step-by-step. The goal is to model your API around the Android API. The central class that includes all the functionality of the SDK is the http://developer.freshdesk.com/mobihelp/android/api/reference/com/freshdesk/mobihelp/Mobihelp.html[com.freshdesk.mobihelp.Mobihelp class], so you begin there. @@ -2275,7 +2275,7 @@ In any case, you don't have to come up with the specifics right now, as you're s ===== Callbacks -it's often the case that native code needs to call back into Codename One code when an event occurs. This may be connected directly to an API method call (for example: as the result of an asynchronous method invocation), or due to something initiated by the operating system or the native SDK on its own (for example: a push notification, a location event, etc..). +It's often the case that native code needs to call back into Codename One code when an event occurs. This may be connected directly to an API method call (for example: as the result of an asynchronous method invocation), or due to something initiated by the operating system or the native SDK on its own (for example: a push notification, a location event, etc..). Native code will have access to both the Codename One API and any native APIs in your app, but on some platforms, accessing the Codename One API may be a little tricky. For example: on iOS you'll be calling from Objective-C back into Java which requires knowledge of Codename One's java-to-goal C conversion process. In general, the easiest way to ease callbacks is to provide abstractions that involve static Java methods (in Codename One space) that accept and return primitive types. @@ -2292,9 +2292,9 @@ The interface definition for `UnreadUpdatesCallback` is: include::../demos/common/src/main/java/com/codenameone/developerguide/advancedtopics/AppArgSnippet.java[tag=appArg,indent=0] ---- -that's: If you were to implement this method (planned for a later section), you need to have a way for the native code to call the `callback.onResult()` method of the passed parameter. +In other words: if you were to implement this method (planned for a later section), you need to have a way for the native code to call the `callback.onResult()` method of the passed parameter. -you've two issues that will need to be solved here: +You've two issues that will need to be solved here: 1. How to pass the `callback` object through the native interface. 2. How to *call* the `callback.onResult()` method from native code at the right time. @@ -2323,7 +2323,7 @@ Note, but, that FreshDesk (and most other service provides that have native SDKs Your public API therefore needs to enable you to provide many credentials in the same app, and your API needs to know to use the correct credentials depending on the device that the app is running on. -many solutions to this problem exist, but the chosen approach provides two different `init()` methods: +Many solutions to this problem exist, but the chosen approach provides two different `init()` methods: [source,java] ---- @@ -2364,7 +2364,7 @@ Notice also, that the native interface includes a set of methods with names pref ===== Connecting the public API to the native interface -you've a public API, and you've a native interface. The idea is that the public API should be a thin wrapper around the native interface to smooth out rough edges that are likely to exist due to the strict set of rules involved in native interfaces. You will, so, use delegation inside the `Mobihelp` class to provide it a reference to an instance of `MobihelpNative`: +You've a public API, and you've a native interface. The idea is that the public API should be a thin wrapper around the native interface to smooth out rough edges that are likely to exist due to the strict set of rules involved in native interfaces. You will, so, use delegation inside the `Mobihelp` class to provide it a reference to an instance of `MobihelpNative`: [source,java] ---- @@ -2490,7 +2490,7 @@ appcompat-v7-19.1.0.aar.md5 appcompat-v7-19.1.0.pom.md5 appcompat-v7-19.1.0.aar.sha1 appcompat-v7-19.1.0.pom.sha1 ---- -two files of interest here: +There are two files of interest here: . appcompat-v7-19.1.0.aar - This is the actual library that you need to include in your project to meet the Mobisdk dependency. . appcompat-v7-19.1.0.pom - This is the Maven XML file for the library. It will show you any dependencies that the appcompat library has. You will also need to include these dependencies: @@ -2506,7 +2506,7 @@ two files of interest here: ---- + -that's: You need to include the `support-v4` library version 19.1.0 in your project. This is also part of the Android Support library. If you back up a couple of directories to: `ANDROID_HOME/sdk/extras/android/m2repository/com/android/support`, you will see it listed there: +In other words: you need to include the `support-v4` library version 19.1.0 in your project. This is also part of the Android Support library. If you back up a couple of directories to: `ANDROID_HOME/sdk/extras/android/m2repository/com/android/support`, you will see it listed there: + ---- appcompat-v7 palette-v7 @@ -2530,7 +2530,7 @@ support-v4-19.1.0-sources.jar.sha1 support-v4-19.1.0.pom.sha1 + Looks like this library is pure Java classes, so you need to include the `support-v4-19.1.0.jar` file into your project. Checking the `.pom` file you see that there are no more dependencies you need to add. -to summarize your findings, you need to include the following files in your `native/android` directory: +To summarize your findings, you need to include the following files in your `native/android` directory: . appcompat-v7-19.1.0.aar . support-v4-19.1.0.jar @@ -2769,7 +2769,7 @@ You can add these dependencies to your project using the `ios.add_libs` build hi .iOS's "add libs" build hint image::img/65e31df8-620c-11e5-87ff-6b926a3f2090.png[iOS's "add libs" build hint,scaledwidth=30%] -that's: you list the framework names separated by semicolons. Notice that your list in the above image doesn't include all the frameworks that they list because many of the frameworks are already included by default (I obtained the default list by building the project with "include sources" checked, then looked at the frameworks that were included). +In short: you list the framework names separated by semicolons. Notice that your list in the above image doesn't include all the frameworks that they list because many of the frameworks are already included by default (I obtained the default list by building the project with "include sources" checked, then looked at the frameworks that were included). === Part 3 : Packaging as a cn1lib @@ -3031,7 +3031,7 @@ Constant pool: ---- -that's a big mess of stuff, but it's pretty easy to pick through it when you know what you're looking for. The layout of this output is pretty straight forward. The beginning shows that this is a class definition: +That's a big mess of stuff, but it's pretty easy to pick through it when you know what you're looking for. The layout of this output is pretty straight forward. The beginning shows that this is a class definition: [source,java] ---- @@ -3102,7 +3102,7 @@ The instruction that *might* be problematic is "invokedynamic." All other instru **Summary of Byte-code Assessment** -to summarize, the byte-code assessment phase, you're basically looking to make sure that the compiler doesn't tend to add dependencies to parts of the JDK that Codename One doesn't support. And you want to make sure that it doesn't use invokedynamic. +To summarize, the byte-code assessment phase, you're basically looking to make sure that the compiler doesn't tend to add dependencies to parts of the JDK that Codename One doesn't support. And you want to make sure that it doesn't use invokedynamic. If you find that the compiler does use invokedynamic or add references to classes that Codename One doesn't support, don't give up yet. You might be able to create your own "porting" runtime library that will provide these dependencies at runtime. @@ -3120,7 +3120,7 @@ This procedure exploits the fact that a cn1lib file is a zip file with a specifi To make the library easier to use the cn1lib file can also contain a file named "stubs.zip" which includes stubs of the Java sources. When you build a cn1lib using a Maven cn1lib project, it will automatically generate stubs of the source so that the IDE will have access to nice things like Javadoc when using the library. The kotlin distribution includes a separate jar file with the runtime sources, named `kotlin-runtime-sources.jar`, so you used this as the "stubs." It contains full sources, which isn't necessary, but it also doesn't hurt. -now that you had your two jar files: kotlin-runtime.jar and kotlin-runtime-sources.jar, create a new empty directory and copy them inside. Rename the jars "main.zip" and "stubs.zip" respectively. Then zip up the directory and rename the zip file `kotlin-runtime.cn1lib`. +Now that you had your two jar files: kotlin-runtime.jar and kotlin-runtime-sources.jar, create a new empty directory and copy them inside. Rename the jars "main.zip" and "stubs.zip" respectively. Then zip up the directory and rename the zip file `kotlin-runtime.cn1lib`. IMPORTANT: Building cn1libs manually in this way is a ** bad habit, as it bypasses the API verification step that occurs when building a library project. it's possible, even likely, that the jar files that you convert depend on classes that aren't in the Codename One library, so your library will fail at runtime in unexpected ways. The reason you could do this with kotlin's runtime (with some confidence) is because the bytecodes were already analyzed to ensure that they didn't include anything problematic. diff --git a/docs/developer-guide/Animations.asciidoc b/docs/developer-guide/Animations.asciidoc index acde9eaab7..7fcf8d47b5 100644 --- a/docs/developer-guide/Animations.asciidoc +++ b/docs/developer-guide/Animations.asciidoc @@ -19,7 +19,7 @@ For example, imagine adding 100 components to a form. If the form was laid out a NOTE: Smart layout reflow logic can alleviate some pains of the automatic layout reflows but since the process is implicit it's almost impossible to optimize complex usages across browsers/devices. A major JavaScript performance tip is to use absolute positioning which is akin to not using layouts at all! -that's why, when you add components to a form that's already showing, you should invoke `revalidate()` or animate the layout appropriately. This also enables the layout animation behavior explained below. +That's why, when you add components to a form that's already showing, you should invoke `revalidate()` or animate the layout appropriately. This also enables the layout animation behavior explained below. [[layout-animations]] === Layout animations @@ -137,7 +137,7 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/animations ===== Animation fade and hierarchy -several more variations on the standard animate methods. Several methods accept a numeric `fade` argument. This is useful to fade out an element in an "unlayout" operation or fade in a regular animation. +There are several more variations on the standard animate methods. Several methods accept a numeric `fade` argument. This is useful to fade out an element in an "unlayout" operation or fade in a regular animation. The value for the fade argument is a number between 0 and 255 where 0 represents full transparency and 255 represents full opacity. @@ -171,7 +171,7 @@ The simple *yet problematic* fix would be: include::../demos/common/src/main/java/com/codenameone/developerguide/animations/AnimationManagerDemo.java[tag=animationManagerWait,indent=0] ---- -why that might still fail? +Why might that still fail? Events come in constantly during the run of the EDT footnote:[Event Dispatch Thread], so an event might come in that might trigger an animation in your code. Even if you're on the EDT keep in mind that you don't actually block it and an event might come in. @@ -241,7 +241,7 @@ https://www.codenameone.com/javadoc/com/codename1/ui/plaf/LookAndFeel.html[LookA **** When defining a transition you define the entering transition and the exiting transition. For most cases only one of those is necessary and you default to the exiting (out transition) as a convention. -for most cases the method `setFormTransitonIn` should go unused. That API exists for some elaborate custom transitions that might need to have a special effect both when transitioning in and out of a specific form. But, most of these effects are easier to achieve with layout animations (for example, components dropping into place etc.). +For most cases the method `setFormTransitonIn` should go unused. That API exists for some elaborate custom transitions that might need to have a special effect both when transitioning in and out of a specific form. But, most of these effects are easier to achieve with layout animations (for example, components dropping into place etc.). For `Dialog` the transition in shows its appearance and the transition out shows its disposal. in that case both transitions make a lot of sense. **** diff --git a/docs/developer-guide/Events.asciidoc b/docs/developer-guide/Events.asciidoc index 170b83cf13..1cbf1161f1 100644 --- a/docs/developer-guide/Events.asciidoc +++ b/docs/developer-guide/Events.asciidoc @@ -7,7 +7,7 @@ working with the higher level events is far more potable since it might map to d === High-level events -high-level events are broadcast using an `addListener`/`setListener` - publish/subscribe system. Most of them are channeled via the https://www.codenameone.com/javadoc/com/codename1/ui/util/EventDispatcher.html[EventDispatcher] class which further simplifies that and makes sending events far easier. +High-level events are broadcast using an `addListener`/`setListener` - publish/subscribe system. Most of them are channeled via the https://www.codenameone.com/javadoc/com/codename1/ui/util/EventDispatcher.html[EventDispatcher] class which further simplifies that and makes sending events far easier. TIP: All events are fired on the Event Dispatch Thread, the `EventDispatcher` makes sure of that. @@ -15,7 +15,7 @@ TIP: All events are fired on the Event Dispatch Thread, the `EventDispatcher` ma Since all events fire on the EDT some complexities occur. E.g.: -you have two listeners monitoring the same event (or related events for example, pointer event and button click event both of which will fire when the button is touched). +You have two listeners monitoring the same event (or related events for example, pointer event and button click event both of which will fire when the button is touched). When the event occurs you can run into a scenario like this: @@ -136,7 +136,7 @@ ConnectionRequest r = new ConnectionRequest() { }; ---- -or you can do something similar using this code: +Or you can do something similar using this code: [source,java] ---- @@ -269,13 +269,13 @@ private void fireEvent(ActionEvent ev) { === Low-level events -low-level events map to "system" events directly. Touch events are considered low-level since they might expose platform specific nuances to your code. +Low-level events map to "system" events directly. Touch events are considered low-level since they might expose platform specific nuances to your code. For example, one platform might send many events during drag while another might send a few. The high-level event handling hides those complexities but some of them trickle down into the low-level event handling. TIP: Codename One tries to hide some complexities from the low-level events as well. But, due to the nature of the event types it's a more challenging task. -low-level events can be bound in one of 3 ways: +Low-level events can be bound in one of 3 ways: * Use one of the add listener methods in https://www.codenameone.com/javadoc/com/codename1/ui/Form.html[Form] for example, `addPointerPressedListener`. @@ -309,7 +309,7 @@ Each of those has advantages and disadvantages, specifically: ==== Low-level event types -two basic types of low-level events: Key and Pointer. +There are two basic types of low-level events: Key and Pointer. IMPORTANT: Key events are relevant to physical keys and won't trigger on virtual keyboard keys, to track those use a https://www.codenameone.com/javadoc/com/codename1/ui/TextField.html[TextField] with a `DataChangeListener` as mentioned above. diff --git a/docs/developer-guide/Index.asciidoc b/docs/developer-guide/Index.asciidoc index 616f798900..8deb915504 100644 --- a/docs/developer-guide/Index.asciidoc +++ b/docs/developer-guide/Index.asciidoc @@ -388,7 +388,7 @@ public void start() { .Title and Label in the UI image::img/codenameone-hello-world-title-label.png[Title and Label in the UI,scaledwidth=50%] -some complex ideas appear within this short snippet that this chapter addresses later when talking about layout. The gist of it's that you create and show a `Form`. `Form` is the top level UI element, it takes over the whole screen. You can add UI elements to that `Form` object, in this case the `Label`. You use the `BoxLayout` to arrange the elements within the `Form` from top to the bottom vertically. +Some complex ideas appear within this short snippet that this chapter addresses later when talking about layout. The gist of it's that you create and show a `Form`. `Form` is the top level UI element, it takes over the whole screen. You can add UI elements to that `Form` object, in this case the `Label`. You use the `BoxLayout` to arrange the elements within the `Form` from top to the bottom vertically. .Application Lifecycle **** @@ -443,7 +443,7 @@ public void destroy() { // <4> <4> `destroy()` is a special case. Under normal circumstances you shouldn't write code in `destroy()`. `stop()` should work for most cases -that's it. You should now have a general sense of the code. it's time to run on the device. +That's it. You should now have a general sense of the code. It's time to run on the device. ==== Building and deploying on devices @@ -452,7 +452,7 @@ image::img/control-center-main.png[Codename One Settings/Control Center,scaledwi You can use the Control Center to configure almost anything. Specifically, the application title, application version, application icon etc. are all found in the Codename One Settings maven target. -many options within this UI that control almost every aspect of the application from signing to basic settings. +There are many options within this UI that control almost every aspect of the application from signing to basic settings. Your device builds using the Codename One Cloud can also be found right here as well as subscription information. @@ -664,7 +664,7 @@ class MyApplication { } ---- -that's pretty familiar. The problem is that there are two bugs in the automatic conversion... that's the code for Kotlin behaves differently from standard Java. +That's pretty familiar. The problem is that there are two bugs in the automatic conversion... that's the code for Kotlin behaves differently from standard Java. The first problem is that Kotlin classes are final unless declared otherwise so you need to add the open keyword before the class declaration as such: @@ -679,7 +679,7 @@ NOTE: This applies to the main class of the project, other classes in Codename O The second problem is that arguments are non-null by default. The `init` method might have a null argument. this fails with an exception. The solution is to add a question mark to the end of the call: `fun init(context: Any?)`. -the full working sample is: +The full working sample is: [source,kotlin] ---- diff --git a/docs/developer-guide/Maven-Creating-CN1Libs.adoc b/docs/developer-guide/Maven-Creating-CN1Libs.adoc index c22de40fa3..5112cd84a0 100644 --- a/docs/developer-guide/Maven-Creating-CN1Libs.adoc +++ b/docs/developer-guide/Maven-Creating-CN1Libs.adoc @@ -313,7 +313,7 @@ This may seem daunting at first, but it's important to realize that 99% of the t ===== Important files -a few key files in this project that you'll be using more than the others. +A few key files in this project that you'll be using more than the others. pom.xml:: The maven configuration file of the root module is where you will set project-wide properties such as the `cn1.version` property, which specifies the version of the Codename One libraries that the module should be compiled against. Periodically, you'll want to update the `cn1.version` property to point to the latest version. diff --git a/docs/developer-guide/Maven-Getting-Started.adoc b/docs/developer-guide/Maven-Getting-Started.adoc index 480410f09f..5818dd7ade 100644 --- a/docs/developer-guide/Maven-Getting-Started.adoc +++ b/docs/developer-guide/Maven-Getting-Started.adoc @@ -332,7 +332,7 @@ The *correct* `` section, is located near the top of the file. You This is a special marker that's used by some Codename One tooling to help it locate the optimal place to inject dependencies. -**don't REMOVE THIS COMMENT**. Just add your dependency snippet somewhere before or after it. +**Don't REMOVE THIS COMMENT**. Just add your dependency snippet somewhere before or after it. ==== ==== Compatibility with Codename One diff --git a/docs/developer-guide/Maven-Updating-Codename-One.adoc b/docs/developer-guide/Maven-Updating-Codename-One.adoc index c47f2d0e44..9f1ed773f9 100644 --- a/docs/developer-guide/Maven-Updating-Codename-One.adoc +++ b/docs/developer-guide/Maven-Updating-Codename-One.adoc @@ -4,7 +4,7 @@ Codename One releases new versions weekly on Maven central. it's recommended tha You can use the <> to update both the Codename One libraries, and the Codename One dependencies in your project. -for example, [source,bash] +For example, [source,bash] ---- mvn cn:update ---- diff --git a/docs/developer-guide/Miscellaneous-Features.asciidoc b/docs/developer-guide/Miscellaneous-Features.asciidoc index b0ead2449c..08234a48f9 100644 --- a/docs/developer-guide/Miscellaneous-Features.asciidoc +++ b/docs/developer-guide/Miscellaneous-Features.asciidoc @@ -1662,7 +1662,7 @@ public void setProjectBuildHint(String key, String value) {} Both of these allow you to detect if a build hint is set and if not (or if it's set incorrectly) set its value... -if you will use the location API from the simulator and you didn't define `ios.locationUsageDescription` Codename One will implicitly define a string there. The cool thing is that you will now see that string in your settings and you would be able to customize it. +If you will use the location API from the simulator and you didn't define `ios.locationUsageDescription` Codename One will implicitly define a string there. The cool thing is that you will now see that string in your settings and you would be able to customize it. For example, this gets way better than that trivial example! @@ -1711,7 +1711,7 @@ int result = e.run(() -> { }); ---- -a few other variants like `runAndWait` and there is a `kill()` method which stops a thread and releases its resources. +There are a few other variants like `runAndWait` and there is a `kill()` method which stops a thread and releases its resources. === Mouse cursor @@ -1773,7 +1773,7 @@ However, the resource file is a bit of a problematic file. As a binary file if y Still, if you want to keep an eye of every change in the resource file you can switch on the #File# -> #XML Team Mode# which should be on by default. This mode creates a file hierarchy under the `res` directory to match the res file you opened. For example: if you've a file named `src/theme.res` it will create a matching `res/theme.xml` and also nest all the images and resources you use in the res directory. -that's useful as you can edit the files directly and keep track of every file in git. For example, this has two big drawbacks: +That's useful as you can edit the files directly and keep track of every file in git. For example, this has two big drawbacks: - it's flaky - while this mode works it never reached the stability of the regular res file mode - It conflicts - the simulator/device are oblivious to this mode. if you fetch an update you also need to update the res file and you might still have conflicts related to that file diff --git a/docs/developer-guide/Monetization.asciidoc b/docs/developer-guide/Monetization.asciidoc index b124570b5e..8657b96bfc 100644 --- a/docs/developer-guide/Monetization.asciidoc +++ b/docs/developer-guide/Monetization.asciidoc @@ -19,11 +19,11 @@ A special ad unit ID exists for test ads. If you specify `ca-app-pub-39402560999 === In-app purchase -in-app purchase is a helpful way to make app development profitable. Codename One supports in-app purchases of consumable and non-consumable products on Android and iOS. It also supports subscriptions. Even though the concept is simple, in-app purchase involves many moving parts, for subscriptions. +In-app purchase is a helpful way to make app development profitable. Codename One supports in-app purchases of consumable and non-consumable products on Android and iOS. It also supports subscriptions. Even though the concept is simple, in-app purchase involves many moving parts, for subscriptions. ==== The SKU -in-app purchase support centers around the set of SKUs you want to sell. Each product, whether it's a one-month subscription, an upgrade to the "Pro" version, or "10 disco credits," has a SKU (stock-keeping unit). Ideally, you can use the same SKU across every store that sells your app. +In-app purchase support centers around the set of SKUs you want to sell. Each product, whether it's a one-month subscription, an upgrade to the "Pro" version, or "10 disco credits," has a SKU (stock-keeping unit). Ideally, you can use the same SKU across every store that sells your app. ==== Types of products diff --git a/docs/developer-guide/Push-Notifications.asciidoc b/docs/developer-guide/Push-Notifications.asciidoc index 0efa878b6b..08e343469f 100644 --- a/docs/developer-guide/Push-Notifications.asciidoc +++ b/docs/developer-guide/Push-Notifications.asciidoc @@ -81,7 +81,7 @@ Codename One provides a secure REST API for sending push notifications. As an HT ==== Receiving a push notification -two different scenarios to be aware of when it comes to receiving push notifications. If your app is running in the foreground when the message arrives, then it will be passed directly to your `push()` callback. If your app is either in the background, or not running, then the notification will be displayed in your device's notifications. If the user then taps the notification, it will open your app, and the `push()` callback will be run with the contents of the message. +There are two different scenarios to be aware of when it comes to receiving push notifications. If your app is running in the foreground when the message arrives, then it will be passed directly to your `push()` callback. If your app is either in the background, or not running, then the notification will be displayed in your device's notifications. If the user then taps the notification, it will open your app, and the `push()` callback will be run with the contents of the message. Some push message types include hidden content that won't be displayed in your device's notifications. These hidden messages (or portions of messages) are passed directly to the `push()` callback of your app for processing. @@ -473,7 +473,7 @@ public void push(String message) { === Deploying Push-Enabled apps to device -you've implemented the Push callback in your app, and tested it in the push simulator and it works. If you try to deploy your app to a device, you may be disappointed to discover that your app doesn't seem to be receiving push notifications when installed on the device. This is because each platform has its own bureaucracy and hoops that you've to jump through before they will deliver notifications to your app. Read on to find out how to meet their requirements. +You've implemented the Push callback in your app, and tested it in the push simulator and it works. If you try to deploy your app to a device, you may be disappointed to discover that your app doesn't seem to be receiving push notifications when installed on the device. This is because each platform has its own bureaucracy and hoops that you've to jump through before they will deliver notifications to your app. Read on to find out how to meet their requirements. [[push-bureaucracy-android-section]] ==== The push bureaucracy - Android @@ -789,7 +789,7 @@ A normal response, will be an array with results: ----- -many things to notice in the responses above: +There are many things to notice in the responses above: - If the response contains `status=updateId` it means that the GCM server wants you to update the device id to a new device id. You should do that in the database and avoid sending pushes to the old key - iOS doesn't acknowledge device receipt but it does send a `status=inactive` result which you should use to remove the device from the list of devices diff --git a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc index fdd453b30a..443a7451d6 100644 --- a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc +++ b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc @@ -236,7 +236,7 @@ NOTE: `hi` is the name of the parent `Form` in the sample above. ==== Styling dialogs -it's important to style a `Dialog` using https://www.codenameone.com/javadoc/com/codename1/ui/Dialog.html#getDialogStyle--[getDialogStyle()] or +It's important to style a `Dialog` using https://www.codenameone.com/javadoc/com/codename1/ui/Dialog.html#getDialogStyle--[getDialogStyle()] or https://www.codenameone.com/javadoc/com/codename1/ui/Dialog.html#setDialogUIID-java.lang.String-[setDialogUIID] methods rather than styling the dialog object directly. The reason for this is that the `Dialog` is a `Form` that takes up the whole screen. The `Form` that's visible behind the `Dialog` is rendered as a screenshot. customizing the actual `UIID` of the `Dialog` won't produce the desired results. @@ -694,7 +694,7 @@ The code is pretty self-explanatory and more convenient than typical setters/get The validator class supports text component and it should "work." But the cool thing is that it uses the material design convention for error handling! -if you add to the sample above a `Validator`: +If you add to the sample above a `Validator`: [source,java] ---- @@ -765,7 +765,7 @@ The one tiny thing you should notice with the `PickerComponent` is that you don' These varying looks are implemented through a combination of layouts, theme constants and UIID's. The most important UIID's are: `TextComponent`, `FloatingHint` & `TextHint`. -many theme constants related that can manipulate some pieces of this functionality: +There are many related theme constants that can manipulate some pieces of this functionality: - `textComponentErrorColor` a hex RGB color which defaults to null in which case this has no effect. When defined this will change the color of the border and label to the given color to match the material design styling. This implements the red border underline in cases of error and the label text color change - `textComponentErrorLineBorderBool` toggles the material-style underline that appears on validation errors. Set it to `false` if you prefer to supply a different border when errors are shown @@ -868,7 +868,7 @@ f.show(); The ripple effect in material design highlights the location of the finger and grows as a circle to occupy the full area of the component as the user presses the button. -you've the ability to perform a ripple effect by darkening the touched area and growing that in a quick animation. +You can perform a ripple effect by darkening the touched area and growing that in a quick animation. Ripple effect can be applied to any component but you have it turned on for buttons on Android which also applies to things like title commands, side menu elements etc. This might not apply at this moment to lead components like multi-buttons but that might change in the future. @@ -939,7 +939,7 @@ hi.add(cb1).add(cb2).add(cb3).add(cb4).add(rb1).add(rb2).add(rb3); .Toggle button converted sample image::img/components-toggle-buttons.png[Toggle button converted sample,scaledwidth=20%] -that's half the story though, to get the full effect of some cool toggle button UI's you can use a https://www.codenameone.com/javadoc/com/codename1/ui/ComponentGroup.html[ComponentGroup]. This allows you to create a button bar effect with the toggle buttons. +That's half the story though: to get the full effect of some cool toggle button UI's you can use a https://www.codenameone.com/javadoc/com/codename1/ui/ComponentGroup.html[ComponentGroup]. This allows you to create a button bar effect with the toggle buttons. For example: lets enclose the `CheckBox` components in a vertical `ComponentGroup` and the `RadioButton's` in a horizontal group. You can do this by changing the last line of the code above as such: @@ -1398,7 +1398,7 @@ Most of these use cases work best for lists that grow to a larger size, or repre ==== Important - lists and Layout managers When working with lists, you want the list to handle the scrolling (otherwise it will perform badly). This means you should place the list in a non-scrollable container (no parent can be scrollable), notice that the content pane is scrollable by default, so you should disable that. -it's also recommended to place the list in the `CENTER` location of a https://www.codenameone.com/javadoc/com/codename1/ui/layouts/BorderLayout.html[BorderLayout] to produce the most effective results. for example: +It's also recommended to place the list in the `CENTER` location of a https://www.codenameone.com/javadoc/com/codename1/ui/layouts/BorderLayout.html[BorderLayout] to produce the most effective results. For example: [source,java] ---- @@ -1409,7 +1409,7 @@ form.add(BorderLayout.CENTER, myList); ==== MultiList & DefaultListModel -after this long start lets show the first sample of creating a list using the https://www.codenameone.com/javadoc/com/codename1/ui/list/MultiList.html[MultiList]. +After this long start, here is the first sample of creating a list using the https://www.codenameone.com/javadoc/com/codename1/ui/list/MultiList.html[MultiList]. The `MultiList` is a preconfigured list that contains a ready made renderer with defaults that make sense for the most common use cases. It still retains most of the power available to the `List` component but reduces the complexity of one of the hardest things to grasp for most developers: rendering. @@ -1605,7 +1605,7 @@ In this renderer you set the `Label.setFocus(true)` if it's selected, calling to Then you invoke `Label.getAllStyles().setBgTransparency(100)` to give the selection semi transparency, and `0` for full transparency if not selected. -that's still not efficient because you create a new `Label` each time the method is invoked. +That's still not efficient because you create a new `Label` each time the method is invoked. To make the code tighter, keep a reference to the `Component` or extend it as https://www.codenameone.com/javadoc/com/codename1/ui/list/DefaultListCellRenderer.html[DefaultListCellRenderer] does. @@ -2005,7 +2005,7 @@ image::img/components-table-with-spanning.png[Table with spanning and fixed widt To customize the table cell behavior you can derive the `Table` to create a "renderer like" widget, but unlike the list this component is "kept" and used as is. This means you can bind listeners to this component and work with it as you would with any other component in Codename One. -lets fix the example above to include far more capabilities: +The example above can be extended to include far more capabilities: [source,java] ---- Table table = new Table(model) { diff --git a/docs/developer-guide/Theme-Basics.asciidoc b/docs/developer-guide/Theme-Basics.asciidoc index 682e7367ba..48e3be0066 100644 --- a/docs/developer-guide/Theme-Basics.asciidoc +++ b/docs/developer-guide/Theme-Basics.asciidoc @@ -55,7 +55,7 @@ When you select the theme you will see the theme default view. .Theme default view image::img/theme-default-view.png[Theme default view] -many interesting things to notice here the preview section allows you to instantly see the changes you make to the theme data. +There are many interesting things to notice here: the preview section allows you to instantly see the changes you make to the theme data. .Theme preview section image::img/theme-preview-section.png[Theme preview section,scaledwidth=50%] @@ -189,7 +189,7 @@ The color settings are much simpler than the background behavior. As explained < .Add theme entry color settings image::img/theme-entry-color.png[Add theme entry color settings] -three color settings: +There are three color settings: - Foreground color is the RRGGBB color that sets the style foreground color used to draw the text of the component. You can use the color picker button on the side to pick a color @@ -243,7 +243,7 @@ A 9-piece image border is a common convention in UI theming that divides a borde TIP: Android uses a common variation on the 9-piece border: 9-patch. The main difference between the 9-piece border and 9-patch is that 9-piece borders tile the sides/center whereas 9-patch scales them -9-piece image borders work better than background images for many use cases where the background needs to "grow/shrink" extensively and might need to change aspect ratio. +The 9-piece image borders work better than background images for many use cases where the background needs to "grow/shrink" extensively and might need to change aspect ratio. They don't work well in cases where the image is asymmetric on both axis. For example: a radial gradient image. 9-piece images in general don't work well with complex gradients. @@ -364,7 +364,7 @@ The `RoundRectBorder` was developed based on the `RoundBorder` and has similar f TIP: don't confuse the Rounded Rectangle border with the deprecated `Rounded` border... -it's a pretty simple border type akin to the `RoundBorder`. +It's a pretty simple border type akin to the `RoundBorder`. .Rounded Rectangle Border image::img/rounded-rectangle-border.png[Rounded Rectangle Border,scaledwidth=50%] diff --git a/docs/developer-guide/Working-With-Javascript.asciidoc b/docs/developer-guide/Working-With-Javascript.asciidoc index bfa34faebb..19b522992b 100644 --- a/docs/developer-guide/Working-With-Javascript.asciidoc +++ b/docs/developer-guide/Working-With-Javascript.asciidoc @@ -190,7 +190,7 @@ If you don't want to install the.war file, but would rather copy the proxy servl ==== Step 2: Configuring your application to use the proxy -three ways to configure your application to use your proxy. +There are three ways to configure your application to use your proxy. 1. Using the *javascript.proxy.url* build hint. + diff --git a/docs/developer-guide/Working-With-iOS.asciidoc b/docs/developer-guide/Working-With-iOS.asciidoc index 91b4b46535..8bd904ad8c 100644 --- a/docs/developer-guide/Working-With-iOS.asciidoc +++ b/docs/developer-guide/Working-With-iOS.asciidoc @@ -311,6 +311,6 @@ Supported need formats are `from:`, `exact:`, `branch:`, `revision:`, and `range If you need to use a dynamic framework (for example: SomeThirdPartySDK.framework), and it isn't available through CocoaPods, then you can add it to your project by zipping up the framework and copying it to your native/ios directory. -for example: native/ios/SomeThirdPartySDK.framework.zip +For example: native/ios/SomeThirdPartySDK.framework.zip -no build hints necessary for this approach. The build server will automatically detect the framework and link it into your app. +No build hints necessary for this approach. The build server will automatically detect the framework and link it into your app. diff --git a/docs/developer-guide/basics.asciidoc b/docs/developer-guide/basics.asciidoc index ad65137432..71a0ce41f5 100644 --- a/docs/developer-guide/basics.asciidoc +++ b/docs/developer-guide/basics.asciidoc @@ -96,7 +96,7 @@ Scrolling doesn't work well for all layout types because the positioning algorit **** Only one element can be scrollable within the hierarchy. Otherwise, if you drag your finger over the `Form`, Codename One can't know which element you want to scroll. By default, the form's content pane scrolls on the Y axis unless you disable it explicitly. Setting the layout to `BorderLayout` disables scrolling implicitly. -it's fine to use non-scrollable layouts such as `BorderLayout` inside a scrollable container. In the TodoApp, for example, `TodoItem` uses `BorderLayout` inside a scrollable `BoxLayout` `Form`. +It's fine to use non-scrollable layouts such as `BorderLayout` inside a scrollable container. In the TodoApp, for example, `TodoItem` uses `BorderLayout` inside a scrollable `BoxLayout` `Form`. **** Layouts can be divided into two distinct groups: @@ -608,7 +608,7 @@ hi.add(LayeredLayout.encloseIn(settingsLabel, FlowLayout.encloseRight(close))); ---- -you're doing three distinct things here: +You're doing three distinct things here: . you're adding a layered layout to the form . you're creating a layered layout and placing two components within. This would be the equivalent of creating a `LayeredLayout` `Container` and invoking `add` twice @@ -1123,7 +1123,7 @@ When you select the component you placed you can edit the properties of that com .Properties allow you to customize everything about a component image::img/new-gui-builder-property-sheet.png[Properties allow you to customize everything about a component,scaledwidth=50%] -five property sheets per component: +There are five property sheets per component: - #Basic Settings# -- These include the basic configuration for a component for example: name, icon, text etc. diff --git a/docs/developer-guide/css.asciidoc b/docs/developer-guide/css.asciidoc index 86eaec90e0..a20fe2fe69 100644 --- a/docs/developer-guide/css.asciidoc +++ b/docs/developer-guide/css.asciidoc @@ -84,7 +84,7 @@ The `#Device` selector allows you to define which device resolutions this CSS fi The `#Constants` selector allows you to specify theme constants. -for example, [source,css] +For example, [source,css] ---- #Constants { PopupDialogArrowBool: false; @@ -155,7 +155,7 @@ In the above example, the constants referring to an image name as a string requi * `../res//` * `../../res//` -*or* that it has been defined as a background image in some selector in this CSS file. +It must also have been defined as a background image in some selector in this CSS file. ==== `Default` @@ -627,7 +627,7 @@ It will then create a multi-image from these images and include them in the reso ==== Import multiple images in single selector -it's quite useful to be able to embed images inside the resource file that's generated from the CSS stylesheet so that you can access the images using the `Resources.getImage()` method in your app and set it as an icon on a button or label. In this case, it's easier to create a dummy style that you don't intend to use and include multiple images in the background-image property like so: +It's quite useful to be able to embed images inside the resource file that's generated from the CSS stylesheet so that you can access the images using the `Resources.getImage()` method in your app and set it as an icon on a button or label. In this case, it's easier to create a dummy style that you don't intend to use and include multiple images in the background-image property like so: [source,css] ---- @@ -836,7 +836,7 @@ NOTE: Apparently FontAwesome has removed its public repositories from Github so ==== `font-size` -it's best practice to size your fonts using millimetres (`rem`) (or another "real-world" measurement unit such as inches (`in`), centimetres (`cm`), millimetres (`mm`). This will allow the font to be sized appropriate for all display densities. If you specify size in pixels (`px`), it will treat it the same as if you sized it in points (`pt`), where 1pt == 1/72 inches (one seventy-second of an inch). +It's best practice to size your fonts using millimetres (`rem`) (or another "real-world" measurement unit such as inches (`in`), centimetres (`cm`), millimetres (`mm`). This will allow the font to be sized appropriate for all display densities. If you specify size in pixels (`px`), it will treat it the same as if you sized it in points (`pt`), where 1pt == 1/72 inches (one seventy-second of an inch). If you size your font in percentage units (for example, `150%`) it will set the font size relative to the medium font size of the platform. This is different than the standard behaviour of a web browser, which would size it relative to the parent element's font size. diff --git a/docs/developer-guide/graphics.asciidoc b/docs/developer-guide/graphics.asciidoc index a7e3151030..05658b9fb8 100644 --- a/docs/developer-guide/graphics.asciidoc +++ b/docs/developer-guide/graphics.asciidoc @@ -143,7 +143,7 @@ Essentially the glass pane is a painter that allows you to draw an overlay on to Overriding the paint method of a form isn't a substitute for `glasspane` as it would appear to work initially, when you enter a `Form`. For example, when modifying an element within the form that element gets repainted not the entire `Form`! -if you've a form with a https://www.codenameone.com/javadoc/com/codename1/ui/Button.html[Button] and text drawn on top using the Form's paint method it would get erased whenever the button gets focus. +If you've a form with a https://www.codenameone.com/javadoc/com/codename1/ui/Button.html[Button] and text drawn on top using the Form's paint method it would get erased whenever the button gets focus. The glass pane is called whenever a component gets painted, it paints within the clipping region of the component hence it won't break the rest of the components on the `Form` which weren't modified. @@ -413,7 +413,7 @@ you need a fallback path. You can check if a particular https://www.codenameone.com/javadoc/com/codename1/ui/Graphics.html[Graphics] context supports rotation and scaling using the `isAffineSupported()` method. -for example: +For example: [source,java] ---- @@ -436,7 +436,7 @@ in Codename One's graphics: 2. Using `Graphics.translate()` to translate your drawing position by an offset. 3. Using `Graphics.rotate()` to rotate your drawing position. -three separate things that need to be drawn in a clock: +There are three separate things that need to be drawn in a clock: 1. **The tick marks**. For example: most clocks will have a tick mark for each second, larger tick marks for each hour, and sometimes even larger tick marks for each quarter hour. @@ -805,7 +805,7 @@ public void stop(){ } ---- -the code to instantiate the clock, and start the animation would be something like: +The code to instantiate the clock, and start the animation would be something like: [source,java] ---- @@ -1067,7 +1067,7 @@ You could also use the `Graphics.setTransform()` class to apply rotations and ot ==== Global alpha & Anti-Aliasing -far you've relied on the per-pixel alpha stored in images and gradients. `Graphics` +Up to this point you've relied on the per-pixel alpha stored in images and gradients. `Graphics` also lets you apply a global alpha multiplier to every draw call by using `setAlpha(int)` or `concatenateAlpha(int)` after checking `isAlphaSupported()`. Both methods accept values from `0` (fully transparent) to `255` (fully opaque) @@ -1149,7 +1149,7 @@ NOTE: This isn't the case for all images but it's common and you prefer calculat ==== The RGB image's -two types of RGB constructed images that are different from one another but since they're both technically "RGB image's" you're bundling them under the same subsection. +There are two types of RGB constructed images that are different from one another but since they're both technically "RGB image's" you're bundling them under the same subsection. ===== Internal @@ -1202,7 +1202,7 @@ Multi images don't physically exist as a concept within the Codename One API so The built-in support for multi images is in the resource file loading logic where a MultiImage is decoded and the version that matches the current DPI is physically loaded. From that point on user code can treat it like any other `EnclodedImage`. -9-image borders use multi images by default to keep their appearance more refined on the different DPI’s. +The 9-image borders use multi images by default to keep their appearance more refined on the different DPI’s. ==== FontImage & Material design icons @@ -1230,7 +1230,7 @@ NOTE: The samples use the built-in material design icon font. This is for conven A more common and arguably "correct" way to construct such an icon would be thru the https://www.codenameone.com/javadoc/com/codename1/ui/plaf/Style.html[Style] object. The `Style` object can provide the color, size and background information needed by `FontImage`. -two versions of this method, the first one expects the `Style` object to have the correct icon font set to its font attribute. The second accepts a `Font` object as an argument. The latter is useful for a case where you want to reuse the same `Style` object that you defined for a general UI element for example: you can set an icon for a `Button` like this and it will take up the style of the `Button`: +There are two versions of this method: the first one expects the `Style` object to have the correct icon font set to its font attribute. The second accepts a `Font` object as an argument. The latter is useful for a case where you want to reuse the same `Style` object that you defined for a general UI element for example: you can set an icon for a `Button` like this and it will take up the style of the `Button`: [source,java] ---- @@ -1251,9 +1251,9 @@ WARNING: Notice that for this specific version of the method the size of the fon ===== Material design icons -many icon fonts in the web, the field is rather volatile and constantly changing. For example, you wanted to have built-in icons that would allow you to create better looking demos and built-in components. +There are many icon fonts on the web, but the field is rather volatile and constantly changing. For example, you wanted to have built-in icons that would allow you to create better looking demos and built-in components. -that's why you picked the material design icon font for inclusion in the Codename One distribution. It features a stable core set of icons, that aren't IP encumbered. +That's why you picked the material design icon font for inclusion in the Codename One distribution. It features a stable core set of icons, that aren't IP encumbered. You can use the built-in font directly as demonstrated above but there are far better ways to create a material design icon. To find the icon you want you can check out the https://design.google.com/icons/[material design icon gallery]. For example: you used the save icon in the samples above. diff --git a/docs/developer-guide/io.asciidoc b/docs/developer-guide/io.asciidoc index 312ad1bc49..354b18ae40 100644 --- a/docs/developer-guide/io.asciidoc +++ b/docs/developer-guide/io.asciidoc @@ -486,7 +486,7 @@ HTTP supports many "request methods," most commonly `GET` & `POST` but also a fe Arguments in HTTP are passed differently between `GET` and `POST` methods. that's what the `setPost` method in Codename One determines, whether arguments added to the request should be placed using the `GET` semantics or the `POST` semantics. -if you continue your example from above you can do something like this: +If you continue your example from above you can do something like this: [source,java] ---- @@ -577,7 +577,7 @@ Here you can extract the headers one by one to handle complex headers such as co As you noticed above practically all the methods in the `ConectionRequest` throw `IOException`. This allows you to avoid the `try`/`catch` semantics and let the error propagate up the chain so it can be handled uniformly by the application. -two distinct placed where you can handle a networking error: +There are two distinct places where you can handle a networking error: - The `ConnectionRequest` - by overriding callback methods - The `NetworkManager` error handler @@ -1178,7 +1178,7 @@ String first2[] = result.getAsStringArray("//player[position() < 3]/firstname"); String secondLast = result.getAsString("//player[last() - 1]/firstName"); ---- -it's also possible to select parent nodes, by using the ‘..’ expression. For example: +It's also possible to select parent nodes, by using the ‘..’ expression. For example: [source,java] @@ -1193,7 +1193,7 @@ Above, you globally find a lastname element with a value of ‘Hewitt’, then g int id = result.getAsInteger("//player[lastname='Hewitt']/@id"); ---- -it's also possible to nest expressions, for example: +It's also possible to nest expressions, for example: [source,java] @@ -1296,14 +1296,14 @@ For example, when you introduced it you didn't have support for the cache filesy JavaScript already knows how to download and cache images from the web. `URLImage` is actually a step back from the things a good browser can do so why not use the native abilities of the browser when you're running there and fallback to using the cache filesystem if it's available and as a last resort go to storage... -that's what the new method of `URLImage` does: +That's what the new method of `URLImage` does: [source,java] ---- public static Image createCachedImage(String imageName, String url, Image placeholder, int resizeRule); ---- -a few important things you need to notice about this method: +A few important things you need to notice about this method: - It returns *Image* and not *URLImage*. This is crucial. Down casting to `URLImage* will work on the simulator but might fail in some platforms (for example: JavaScript) so don't do that! + Since this is implemented natively in JavaScript you need a different abstraction for that platform. @@ -1388,7 +1388,7 @@ curl 'https://api.twilio.com/2010-04-01/Accounts/[accountSID]/Messages.json' -X -u [accountSID]:[AuthToken] ---- -that's pretty cool as the curl command maps almost directly to the `Rest` API call! +That's pretty cool as the curl command maps almost directly to the `Rest` API call! What you do here is actually pretty trivial, you open a connection the api messages URL. You add arguments to the body of the post request and define the basic authentication data. @@ -1533,7 +1533,7 @@ private static final String DESTINATION_URL = "http://localhost:8080/HelloWebSer You would need to update the host name of the server for running on a device otherwise the device would need to live within your internal network and point to your IP address. -it's now time to write the actual client code that calls this. Every method you defined above is now defined as a static method within the `GameOfThronesService` class with two permutations. One is a synchronous permutation that behaves as expected. It blocks the calling thread while calling the server and might throw an `IOException` if something failed. +It's now time to write the actual client code that calls this. Every method you defined above is now defined as a static method within the `GameOfThronesService` class with two permutations. One is a synchronous permutation that behaves as expected. It blocks the calling thread while calling the server and might throw an `IOException` if something failed. This kind of method (synchronous method) is easy to work with since it's legal to call it from the event dispatch thread and it's easy to map it to application logic flow. @@ -1605,7 +1605,7 @@ default using `setDefaultCacheMode(CachingMode)`. NOTE: Caching applies to `GET` operations, it won't work for `POST` or other methods -many methods of interest to keep an eye for: +There are many methods of interest to keep an eye for: [source,java] ---- @@ -2045,7 +2045,7 @@ public class Meeting { } ---- -that's a classic POJO and it's the force that underlies JavaBeans and a few tools in Java. +That's a classic POJO and it's the force that underlies JavaBeans and a few tools in Java. The properties are effectively the getters/setters for example: `subject`, `when` etc. but properties have many features that are crucial: @@ -2125,7 +2125,7 @@ This means that this code won't compile: meet.subject = otherValue; ---- -all setting/getting must happen thru the set/get methods and they can be replaced. For example: this is valid syntax +All setting/getting must happen thru the set/get methods and they can be replaced. For example: this is valid syntax that prevents setting the property to null and defaults it to an empty string: [source,java] @@ -2147,7 +2147,7 @@ NOTE: You will discuss the reason for returning the `Meeting` instance below Since `Property` is a common class it's pretty easy for introspective code to manipulate properties. For example, it can't detect properties in an object without reflection. -that's why you've the index object and the `PropertyBusinessObject` interface (which defines `getPropertyIndex`). +That's why you've the index object and the `PropertyBusinessObject` interface (which defines `getPropertyIndex`). The `PropertyIndex` class provides meta data for the surrounding class including the list of the properties within. It allows enumerating the properties and iterating over them making them accessible to all tools. @@ -2162,7 +2162,7 @@ meet.subject.addChangeListener((p) -> Log.p("New property value is: " + p.get()) ==== The cool stuff -that's the simple stuff that can be done with properties, but they can do **much** more! +That's the simple stuff that can be done with properties, but they can do **much** more! For starters all the common methods of `Object` can be implemented with almost no code: @@ -2220,7 +2220,7 @@ but sqlite isn't big iron so it might be good enough. One of the problematic issues with constructors is that any change starts propagating everywhere. If you have fields in the constructor and you add a new field later you need to keep the old constructor for compatibility. -you added a new syntax: +You added a new syntax: [source,java] ---- @@ -2229,10 +2229,10 @@ Meeting meet = new Meeting(). when.set(new Date()); ---- -that's why every property in the definition needed the `Meeting` generic and the set method returns the `Meeting` +That's why every property in the definition needed the `Meeting` generic and the set method returns the `Meeting` instance... -you're pretty conflicted on this feature and are thinking about removing it. +You're pretty conflicted on this feature and are thinking about removing it. Without this feature the code would look like this: @@ -2376,7 +2376,7 @@ The arguments for the `select` method are: - Number of elements to fetch - Page to start with - in this case if you've more than 1000 elements you can fetch the next page using `sm.select(c, c.name, true, 1000, 1)` -many more configurations where you can fine tune how a specific property maps to a column etc. +There are many more configurations where you can fine tune how a specific property maps to a column etc. ====== What's still missing @@ -2421,7 +2421,7 @@ Some objects make sense as global objects, you can use the `Preferences` API to PreferencesObject.create(settingsInstance).bind(); ---- -if settings has a property called `companyName` it would bind into `Preferences` under the `Settings.companyName` entry. +If settings has a property called `companyName` it would bind into `Preferences` under the `Settings.companyName` entry. You can do some more elaborate bindings such as: @@ -2571,7 +2571,7 @@ The picker component implicitly works for date type properties, numeric constrai However, how do you know to use an email constraint for the email property? -you've some special case defaults for some common property names, so if your property is named email it will use an email constraint by default. If it's named url or password etc. it will do the "right thing" unless you explicitly state otherwise. You can customize the constraint for a specific property using something like: +You've some special case defaults for some common property names, so if your property is named email it will use an email constraint by default. If it's named url or password etc. it will do the "right thing" unless you explicitly state otherwise. You can customize the constraint for a specific property using something like: [source,java] ---- diff --git a/docs/developer-guide/security.asciidoc b/docs/developer-guide/security.asciidoc index 08c30a7f89..4d2d261fa3 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -31,7 +31,7 @@ Notice that this is a temporary roadblock as any savvy hacker would compile the NOTE: This isn't encoding or encryption — it's a simple obfuscation of the data. -two simple methods in the `Util` class: +There are two simple methods in the `Util` class: [source,java] ---- @@ -220,9 +220,9 @@ That won't work either. All certificates are signed by a "certificate authority" TIP: What if an attacker could get a fake certificate authorized by a real certificate authority? -**that's a problem!** +**That's a problem!** -it's obviously hard to do but if someone was able to do this he could execute a "man in the middle" attack as described above. People were able to fool certificate authorities in the past and gain fake certificates using various methods so this is possible and probably doable for any government level attacker. +It's obviously hard to do but if someone was able to do this he could execute a "man in the middle" attack as described above. People were able to fool certificate authorities in the past and gain fake certificates using various methods so this is possible and probably doable for any government level attacker. ==== Certificate pinning diff --git a/docs/developer-guide/signing.asciidoc b/docs/developer-guide/signing.asciidoc index 89c9a43c4b..455b9f08ee 100644 --- a/docs/developer-guide/signing.asciidoc +++ b/docs/developer-guide/signing.asciidoc @@ -161,7 +161,7 @@ WARNING: You should use the certificate wizard, if you don't have a Mac. This se iOS signing has two distinct modes: App Store signing which is valid for distribution via App Store Connect (you won't be able to run the resulting application without submitting it to Apple) and development mode signing. -you've two major files to keep track of: +You've two major files to keep track of: 1. *Certificate* - your signature 2. *Provisioning Profile* - details about the application and who is allowed to execute it diff --git a/scripts/developer-guide/check_paragraph_capitalization.rb b/scripts/developer-guide/check_paragraph_capitalization.rb new file mode 100755 index 0000000000..f902f192c7 --- /dev/null +++ b/scripts/developer-guide/check_paragraph_capitalization.rb @@ -0,0 +1,138 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# Fails when an AsciiDoc paragraph in the developer guide starts with a +# lowercase word. Catches the class of mistake from PR #5000 (a paragraph +# rendering as "many ways to animate…" with no leading subject). +# +# Vale cannot enforce this reliably because its asciidoc tokenizer fragments +# paragraphs at inline markup (`#kbd#`, code spans, links), making anchor +# regexes match on every inline-separated text run. We use the asciidoctor +# parser instead so paragraph boundaries match the rendered document. + +require 'asciidoctor' +require 'json' +require 'optparse' + +ALLOWED_FIRST_TOKENS = %w[ + iOS iPhone iPad iPod iCloud iMac iTunes + macOS tvOS watchOS visionOS + iframe + eBay +].freeze + +options = { output: nil } +OptionParser.new do |opts| + opts.banner = 'Usage: check_paragraph_capitalization.rb [options] PATH [PATH ...]' + opts.on('--output FILE', 'Write JSON report of all findings to FILE') do |f| + options[:output] = f + end +end.parse! + +paths = ARGV +abort 'no input paths' if paths.empty? + +# Each path is either a master document (with include:: directives) or a +# standalone file. We process the file as given; asciidoctor's source map +# preserves the originating file for each paragraph, so passing only the +# top-level developer-guide.asciidoc avoids double-counting paragraphs that +# appear via include::. +files = paths.flat_map do |path| + if File.directory?(path) + Dir.glob(File.join(path, '*.{adoc,asciidoc}')).sort + else + [path] + end +end + +def strip_html(html) + text = html.gsub(/<[^>]+>/, '') + text.gsub('&', '&') + .gsub('<', '<') + .gsub('>', '>') + .gsub('"', '"') + .gsub(''', "'") + .gsub(' ', ' ') +end + +errors = [] + +files.each do |path| + doc = Asciidoctor.load_file( + path, + sourcemap: true, + safe: :unsafe, + standalone: false, + parse: true + ) + + doc.find_by(context: :paragraph).each do |para| + next unless para.lineno + + rendered = para.content.to_s.strip + next if rendered.empty? + + # Skip paragraphs that begin with a code identifier, keyboard shortcut, + # link, or inline image. These are typically "`Name` — description" + # pseudo-list entries where the leading element is an API symbol whose + # case is determined by the language, not by prose conventions. We also + # accept formatting wrappers (strong/em/b/i/mark/u/sub/sup) around the + # identifier because asciidoctor sometimes preserves them — for example + # `**\`a\` / \`b\`**` renders as `a / b` + # while `**\`a\`**` collapses to `a`. + next if rendered =~ %r{\A(?:<(?:strong|em|b|i|mark|u|sub|sup)>\s*)*<(code|kbd|samp|var|a\b|img\b|span class="image)} + + plain = strip_html(rendered).strip + next if plain.empty? + + # One-word paragraphs are conventional transitional connectors between + # adjacent code blocks ("becomes", "and", "to"). They aren't sentences, + # so capitalization rules don't apply. + next if plain.scan(/\S+/).length <= 1 + + first_word = plain[/[A-Za-z][A-Za-z0-9]*/, 0] + next if first_word.nil? + next unless first_word[0].match?(/[a-z]/) + next if ALLOWED_FIRST_TOKENS.any? { |w| w.casecmp(first_word).zero? } + + # Skip if the first "word" looks like a code identifier that asciidoctor + # rendered as plain text (e.g. a fully-qualified package name without + # backticks). Detection: the first token contains an internal dot or + # camelCase boundary. + extended_token = plain[/[A-Za-z][A-Za-z0-9._]*/, 0] || first_word + next if extended_token.include?('.') || extended_token.match?(/[a-z][A-Z]/) + + source_file = para.file || path + # Normalize to a repo-relative path when possible so reports are stable + # across machines. + if source_file && source_file.start_with?(Dir.pwd + '/') + source_file = source_file.sub(Dir.pwd + '/', '') + end + + errors << { + file: source_file, + line: para.lineno, + word: first_word, + excerpt: plain[0, 120] + } + end +end + +if options[:output] + payload = { total: errors.length, findings: errors } + File.write(options[:output], JSON.pretty_generate(payload)) +end + +if errors.any? + warn "Paragraph capitalization check failed: #{errors.length} paragraph(s) start with a lowercase word." + errors.each do |e| + warn " #{e[:file]}:#{e[:line]}: '#{e[:word]}' — #{e[:excerpt]}" + end + warn '' + warn 'Each flagged paragraph must be rewritten so its first prose word begins with a capital letter.' + warn 'Example: "many ways to animate..." → "There are many ways to animate..."' + exit 1 +end + +puts "Paragraph capitalization check passed: #{files.length} file(s), 0 issue(s)." +exit 0 diff --git a/scripts/developer-guide/run_languagetool.py b/scripts/developer-guide/run_languagetool.py new file mode 100644 index 0000000000..02408d9b3b --- /dev/null +++ b/scripts/developer-guide/run_languagetool.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Run LanguageTool against the rendered developer guide. + +This produces an advisory report only — LanguageTool's signal/noise ratio is +not high enough to gate the build on, but its findings are useful for spot- +checks and as a soft signal in PR review. The companion paragraph +capitalization check (check_paragraph_capitalization.rb) is the hard gate. + +Input: the asciidoctor-rendered HTML for the full developer guide. +Output: JSON report compatible with the existing summarize_reports.py +pipeline, written to the path given by --output. +""" + +import argparse +import json +import os +import sys +from html.parser import HTMLParser + + +SKIP_TAGS = {"script", "style", "code", "pre", "kbd", "samp", "var", "tt"} + + +class TextExtractor(HTMLParser): + """Pull the prose content out of asciidoctor's HTML output.""" + + def __init__(self): + super().__init__() + self._chunks = [] + self._skip_depth = 0 + + def handle_starttag(self, tag, attrs): + if tag in SKIP_TAGS: + self._skip_depth += 1 + elif tag in ("p", "li", "h1", "h2", "h3", "h4", "h5", "h6", "div"): + self._chunks.append("\n\n") + + def handle_endtag(self, tag): + if tag in SKIP_TAGS and self._skip_depth > 0: + self._skip_depth -= 1 + elif tag in ("p", "li", "h1", "h2", "h3", "h4", "h5", "h6"): + self._chunks.append("\n") + + def handle_data(self, data): + if self._skip_depth == 0: + self._chunks.append(data) + + def text(self): + return "".join(self._chunks) + + +def extract_text(html_path): + parser = TextExtractor() + with open(html_path, "r", encoding="utf-8") as fh: + parser.feed(fh.read()) + return parser.text() + + +CHUNK_BYTES = 40_000 + + +def chunk_text(text, max_bytes=CHUNK_BYTES): + """Split text on paragraph boundaries into chunks under max_bytes each. + + The local LanguageTool server crashes ('Connection reset by peer') when + fed multi-megabyte inputs in a single request, so we batch by paragraph. + Yields (offset, chunk_text) pairs so callers can translate per-chunk + offsets back to a global offset. + """ + paragraphs = text.split("\n\n") + buf = [] + buf_len = 0 + offset = 0 + chunk_start = 0 + for para in paragraphs: + segment = para + "\n\n" + if buf and buf_len + len(segment) > max_bytes: + yield chunk_start, "".join(buf) + chunk_start = offset + buf = [segment] + buf_len = len(segment) + else: + buf.append(segment) + buf_len += len(segment) + offset += len(segment) + if buf: + yield chunk_start, "".join(buf) + + +def run_languagetool(text, language="en-US"): + try: + import language_tool_python + except ImportError: + print( + "language_tool_python is not installed; skipping LanguageTool check.", + file=sys.stderr, + ) + return None + + tool = language_tool_python.LanguageTool(language) + all_matches = [] + try: + for global_offset, chunk in chunk_text(text): + try: + matches = tool.check(chunk) + except Exception as exc: # noqa: BLE001 — advisory check must not crash CI + print( + f"LanguageTool failed on chunk at offset {global_offset} ({len(chunk)} bytes): {exc}", + file=sys.stderr, + ) + continue + for m in matches: + # Wrap match so callers see global offsets, not chunk-local. + m._global_offset = global_offset + m.offset + all_matches.append(m) + finally: + tool.close() + return all_matches + + +def _attr(obj, *names, default=None): + """Read an attribute by the first matching name. + + language_tool_python renamed its Match accessors from camelCase + (ruleId, errorLength) to snake_case (rule_id, error_length) between + versions; CI pins 2.9.4 (camelCase) while local dev may have a newer + release. Try both so the script works on either. + """ + for name in names: + try: + val = getattr(obj, name) + except AttributeError: + continue + if val is not None: + return val + return default + + +def matches_to_json(matches, text): + out = [] + for m in matches: + # Translate offset to a line number in the plain-text input. + offset = getattr(m, "_global_offset", _attr(m, "offset", default=0)) + line = text.count("\n", 0, offset) + 1 + out.append({ + "rule": _attr(m, "rule_id", "ruleId", default=""), + "category": _attr(m, "category", default=""), + "message": _attr(m, "message", default=""), + "line": line, + "offset": offset, + "length": _attr(m, "error_length", "errorLength", default=0), + "context": _attr(m, "context", default=""), + "replacements": list(_attr(m, "replacements", default=[])[:5]), + }) + return out + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--html", required=True, help="Rendered HTML input") + parser.add_argument("--output", required=True, help="JSON output path") + parser.add_argument("--language", default="en-US") + args = parser.parse_args() + + text = extract_text(args.html) + + report = {"status": "unknown", "matches": [], "total": 0} + try: + try: + matches = run_languagetool(text, language=args.language) + except Exception as exc: # noqa: BLE001 — advisory check must not crash CI + print(f"LanguageTool failed to start: {exc}", file=sys.stderr) + report = {"status": "error", "reason": str(exc), "matches": [], "total": 0} + else: + if matches is None: + report = { + "status": "skipped", + "reason": "language_tool_python not installed", + "matches": [], + "total": 0, + } + else: + try: + serialized = matches_to_json(matches, text) + except Exception as exc: # noqa: BLE001 + print( + f"LanguageTool: failed to serialize {len(matches)} match(es): {exc}", + file=sys.stderr, + ) + report = { + "status": "error", + "reason": f"serialization failed: {exc}", + "matches": [], + "total": len(matches), + } + else: + report = {"status": "ok", "matches": serialized, "total": len(serialized)} + finally: + os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True) + with open(args.output, "w", encoding="utf-8") as fh: + json.dump(report, fh, indent=2) + print( + f"LanguageTool report written to {args.output} " + f"({report.get('total', 0)} match(es), status={report.get('status')})." + ) + + # Advisory check: never fails the build. + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/developer-guide/summarize_reports.py b/scripts/developer-guide/summarize_reports.py index e234035c4f..9db07802a3 100644 --- a/scripts/developer-guide/summarize_reports.py +++ b/scripts/developer-guide/summarize_reports.py @@ -165,6 +165,64 @@ def summarize_vale( write_outputs([(summary_key, summary)], output) +def summarize_paragraph_capitalization( + report: Path, status: str, summary_key: str, output: Path | None +) -> None: + total = 0 + if report.is_file(): + try: + data = json.loads(report.read_text(encoding="utf-8")) + except json.JSONDecodeError: + data = {} + if isinstance(data, dict): + total = int(data.get("total", 0) or 0) + + if total or _has_nonzero_status(status): + summary = f"{total} paragraph(s) starting with a lowercase word" + else: + summary = "No paragraph capitalization issues" + + write_outputs([(summary_key, summary)], output) + + +def summarize_languagetool( + report: Path, summary_key: str, output: Path | None +) -> None: + total = 0 + status = "missing" + top_rules: list[tuple[str, int]] = [] + if report.is_file(): + try: + data = json.loads(report.read_text(encoding="utf-8")) + except json.JSONDecodeError: + data = {} + if isinstance(data, dict): + total = int(data.get("total", 0) or 0) + status = str(data.get("status", "unknown")) + matches = data.get("matches", []) or [] + if isinstance(matches, list): + counts: dict[str, int] = {} + for m in matches: + if isinstance(m, dict): + rule = str(m.get("rule", "?")) + counts[rule] = counts.get(rule, 0) + 1 + top_rules = sorted(counts.items(), key=lambda kv: -kv[1])[:3] + + if status == "error": + summary = "LanguageTool failed to run (advisory only)" + elif status == "skipped": + summary = "LanguageTool skipped (advisory only)" + elif total: + rules_hint = "" + if top_rules: + rules_hint = " — top: " + ", ".join(f"{r} ({c})" for r, c in top_rules) + summary = f"{total} advisory match(es){rules_hint}" + else: + summary = "No grammar matches" + + write_outputs([(summary_key, summary)], output) + + def summarize_unused_images( report: Path, summary_key: str, @@ -246,6 +304,23 @@ def parse_args() -> argparse.Namespace: unused_parser.add_argument("--details-key", default=None) unused_parser.add_argument("--preview-limit", type=int, default=10) + paragraph_parser = subparsers.add_parser( + "paragraph-capitalization", + help="Summarize paragraph capitalization check results.", + parents=[common], + ) + paragraph_parser.add_argument("--report", type=Path, required=True) + paragraph_parser.add_argument("--status", default="0") + paragraph_parser.add_argument("--summary-key", default="summary") + + lt_parser = subparsers.add_parser( + "languagetool", + help="Summarize LanguageTool advisory report results.", + parents=[common], + ) + lt_parser.add_argument("--report", type=Path, required=True) + lt_parser.add_argument("--summary-key", default="summary") + return parser.parse_args() @@ -265,6 +340,12 @@ def main() -> None: args.preview_limit, output, ) + elif command == "paragraph-capitalization": + summarize_paragraph_capitalization( + args.report, args.status, args.summary_key, output + ) + elif command == "languagetool": + summarize_languagetool(args.report, args.summary_key, output) else: raise ValueError(f"Unsupported command: {command}")