com.wallstop-studios.unity-helpers 2.0.0-rc81.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +1 -1
- package/.gitattributes +1 -1
- package/.githooks/pre-commit +31 -5
- package/.githooks/pre-push +50 -0
- package/.github/dependabot.yml +24 -2
- package/.github/scripts/check-markdown-links.ps1 +77 -0
- package/.github/scripts/check_markdown_links.py +89 -0
- package/.github/scripts/check_markdown_url_encoding.py +74 -0
- package/.github/scripts/validate_markdown_links.py +194 -0
- package/.github/workflows/csharpier-autofix.yml +152 -0
- package/.github/workflows/format-on-demand.yml +305 -0
- package/.github/workflows/lint-doc-links.yml +8 -5
- package/.github/workflows/markdown-json.yml +6 -2
- package/.github/workflows/npm-publish.yml +1 -1
- package/.github/workflows/prettier-autofix.yml +195 -0
- package/.github/workflows/update-dotnet-tools.yml +80 -0
- package/.github/workflows/yaml-format-lint.yml +41 -0
- package/.lychee.toml +4 -4
- package/.markdownlint.jsonc +21 -0
- package/.pre-commit-config.yaml +11 -3
- package/.yamllint.yaml +31 -0
- package/AGENTS.md +5 -1
- package/Docs/CHANGELOG.md +11 -0
- package/Docs/CONTRIBUTING.md +49 -0
- package/Docs/CONTRIBUTING.md.meta +7 -0
- package/{EDITOR_TOOLS_GUIDE.md → Docs/EDITOR_TOOLS_GUIDE.md} +4 -0
- package/Docs/EFFECTS_SYSTEM.md +1316 -0
- package/{EFFECTS_SYSTEM_TUTORIAL.md → Docs/EFFECTS_SYSTEM_TUTORIAL.md} +1 -1
- package/{GETTING_STARTED.md → Docs/GETTING_STARTED.md} +50 -64
- package/{GLOSSARY.md → Docs/GLOSSARY.md} +4 -4
- package/Docs/HELPER_UTILITIES.md +885 -0
- package/Docs/HELPER_UTILITIES.md.meta +7 -0
- package/{INDEX.md → Docs/INDEX.md} +107 -62
- package/Docs/MATH_AND_EXTENSIONS.md +1039 -0
- package/{RANDOM_PERFORMANCE.md → Docs/RANDOM_PERFORMANCE.md} +15 -15
- package/{RELATIONAL_COMPONENTS.md → Docs/RELATIONAL_COMPONENTS.md} +111 -84
- package/{SERIALIZATION.md → Docs/SERIALIZATION.md} +15 -0
- package/{SPATIAL_TREES_2D_GUIDE.md → Docs/SPATIAL_TREES_2D_GUIDE.md} +2 -2
- package/{SPATIAL_TREES_3D_GUIDE.md → Docs/SPATIAL_TREES_3D_GUIDE.md} +1 -1
- package/Docs/SPATIAL_TREE_2D_PERFORMANCE.md +241 -0
- package/Docs/SPATIAL_TREE_3D_PERFORMANCE.md +243 -0
- package/{THIRD_PARTY_NOTICES.md → Docs/THIRD_PARTY_NOTICES.md} +1 -1
- package/Docs/UTILITY_COMPONENTS.md +906 -0
- package/Docs/UTILITY_COMPONENTS.md.meta +7 -0
- package/Docs/VISUAL_COMPONENTS.md +337 -0
- package/Docs/VISUAL_COMPONENTS.md.meta +7 -0
- package/Editor/AnimationEventEditor.cs +337 -160
- package/Editor/Core/Helper/AnimationEventHelpers.cs +178 -152
- package/Editor/CustomEditors/PersistentDirectoryGUI.cs +20 -11
- package/Editor/CustomEditors/TexturePlatformOverrideEntryDrawer.cs +11 -2
- package/Editor/FitTextureSizeWindow.cs +43 -19
- package/Editor/PersistentDirectorySettings.cs +64 -12
- package/Editor/PrefabChecker.cs +72 -5
- package/Editor/Sprites/AnimationCopier.cs +131 -55
- package/Editor/Sprites/AnimationCreator.cs +63 -22
- package/Editor/Sprites/AnimationViewerWindow.cs +42 -6
- package/Editor/Sprites/TexturePlatformNameHelper.cs +50 -39
- package/Editor/Sprites/TextureResizerWizard.cs +23 -1
- package/Editor/Sprites/TextureSettingsApplierWindow.cs +148 -85
- package/Editor/Tools/ImageBlurTool.cs +81 -10
- package/Editor/Utils/EditorUi.cs +1 -1
- package/Editor/Utils/ScriptableObjectSingletonCreator.cs +1 -1
- package/README.md +428 -2433
- package/Runtime/AssemblyInfo.cs +4 -0
- package/Runtime/Core/Attributes/NotNullAttribute.cs +1 -3
- package/Runtime/Core/Attributes/RelationalComponentAssigner.cs +50 -5
- package/Runtime/Core/DataStructure/CyclicBuffer.cs +0 -1
- package/Runtime/Core/DataStructure/KDTree3D.cs +1 -1
- package/Runtime/Core/DataStructure/OctTree3D.cs +1 -1
- package/Runtime/Core/Extension/AsyncOperationExtensions.cs +122 -0
- package/Runtime/Core/Extension/RandomExtensions.cs +68 -0
- package/Runtime/Core/Extension/WallstopStudiosLogger.cs +16 -0
- package/Runtime/Core/Helper/Partials/ObjectHelpers.cs +4 -1
- package/Runtime/Core/Helper/ReflectionHelpers.cs +21 -10
- package/Runtime/Core/Helper/SpriteHelpers.cs +3 -1
- package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +45 -1
- package/Runtime/Core/Serialization/JsonConverters/GameObjectConverter.cs +13 -5
- package/Runtime/Core/Serialization/JsonConverters/ResolutionConverter.cs +1 -1
- package/Runtime/Core/Serialization/JsonConverters/TypeConverter.cs +1 -1
- package/Runtime/Core/Serialization/ProtobufUnitySurrogates.cs +24 -29
- package/Runtime/Core/Serialization/Serializer.cs +101 -0
- package/Runtime/Integrations/Reflex/AssemblyInfo.cs +7 -0
- package/Runtime/Integrations/Reflex/AssemblyInfo.cs.meta +11 -0
- package/Runtime/Integrations/Reflex/ContainerRelationalExtensions.cs +198 -0
- package/Runtime/Integrations/Reflex/ContainerRelationalExtensions.cs.meta +11 -0
- package/Runtime/Integrations/Reflex/RelationalComponentsInstaller.cs +86 -0
- package/Runtime/Integrations/Reflex/RelationalComponentsInstaller.cs.meta +11 -0
- package/Runtime/Integrations/Reflex/RelationalReflexSceneBootstrapper.cs +316 -0
- package/Runtime/Integrations/Reflex/RelationalReflexSceneBootstrapper.cs.meta +11 -0
- package/Runtime/Integrations/Reflex/RelationalSceneAssignmentOptions.cs +86 -0
- package/Runtime/Integrations/Reflex/RelationalSceneAssignmentOptions.cs.meta +11 -0
- package/Runtime/Integrations/Reflex/WallstopStudios.UnityHelpers.Integration.Reflex.asmdef +20 -0
- package/Runtime/Integrations/Reflex/WallstopStudios.UnityHelpers.Integration.Reflex.asmdef.meta +7 -0
- package/Runtime/Integrations/Reflex.meta +8 -0
- package/Runtime/Integrations/VContainer/AssemblyInfo.cs +9 -0
- package/Runtime/Integrations/VContainer/AssemblyInfo.cs.meta +3 -0
- package/Runtime/Integrations/VContainer/ObjectResolverRelationalExtensions.cs +96 -0
- package/Runtime/Integrations/VContainer/RelationalComponentEntryPoint.cs +90 -10
- package/Runtime/Integrations/VContainer/RelationalComponentsBuilderExtensions.cs +13 -1
- package/Runtime/Integrations/VContainer/RelationalObjectPools.cs +114 -0
- package/Runtime/Integrations/VContainer/RelationalObjectPools.cs.meta +11 -0
- package/Runtime/Integrations/VContainer/RelationalSceneAssignmentOptions.cs +16 -4
- package/Runtime/Integrations/VContainer/RelationalSceneLoadListener.cs +241 -0
- package/Runtime/Integrations/VContainer/RelationalSceneLoadListener.cs.meta +11 -0
- package/Runtime/Integrations/Zenject/AssemblyInfo.cs +9 -0
- package/Runtime/Integrations/Zenject/AssemblyInfo.cs.meta +3 -0
- package/Runtime/Integrations/Zenject/DiContainerRelationalExtensions.cs +69 -2
- package/Runtime/Integrations/Zenject/RelationalComponentSceneInitializer.cs +89 -12
- package/Runtime/Integrations/Zenject/RelationalComponentsInstaller.cs +23 -1
- package/Runtime/Integrations/Zenject/RelationalMemoryPools.cs +44 -0
- package/Runtime/Integrations/Zenject/RelationalMemoryPools.cs.meta +11 -0
- package/Runtime/Integrations/Zenject/RelationalSceneAssignmentOptions.cs +16 -10
- package/Runtime/Integrations/Zenject/RelationalSceneLoadListener.cs +243 -0
- package/Runtime/Integrations/Zenject/RelationalSceneLoadListener.cs.meta +11 -0
- package/Runtime/Tags/AttributeMetadataCache.cs +1 -4
- package/Runtime/Utils/Buffers.cs +4 -4
- package/Runtime/Utils/ScriptableObjectSingleton.cs +1 -2
- package/Runtime/Utils/SetTextureImportData.cs +3 -1
- package/Runtime/Utils/TextureScale.cs +10 -2
- package/Runtime/Visuals/UGUI/EnhancedImage.cs +6 -0
- package/Runtime/Visuals/UIToolkit/LayeredImage.cs +4 -1
- package/Samples~/DI - Reflex/README.md +527 -0
- package/Samples~/DI - Reflex/README.md.meta +7 -0
- package/Samples~/DI - Reflex/Scripts/ReflexPaletteService.cs +36 -0
- package/Samples~/DI - Reflex/Scripts/ReflexPaletteService.cs.meta +11 -0
- package/Samples~/DI - Reflex/Scripts/ReflexRelationalConsumer.cs +79 -0
- package/Samples~/DI - Reflex/Scripts/ReflexRelationalConsumer.cs.meta +11 -0
- package/Samples~/DI - Reflex/Scripts/ReflexSampleInstaller.cs +30 -0
- package/Samples~/DI - Reflex/Scripts/ReflexSampleInstaller.cs.meta +11 -0
- package/Samples~/DI - Reflex/Scripts/ReflexSpawner.cs +79 -0
- package/Samples~/DI - Reflex/Scripts/ReflexSpawner.cs.meta +11 -0
- package/Samples~/DI - Reflex/Scripts/Samples.UnityHelpers.DI.Reflex.asmdef +26 -0
- package/Samples~/DI - Reflex/Scripts/Samples.UnityHelpers.DI.Reflex.asmdef.meta +9 -0
- package/Samples~/DI - Reflex/Scripts.meta +8 -0
- package/Samples~/DI - Reflex.meta +8 -0
- package/Samples~/DI - VContainer/README.md +238 -56
- package/Samples~/DI - VContainer/Scripts/GameLifetimeScope.cs +22 -4
- package/Samples~/DI - VContainer/Scripts/RelationalConsumer.cs +5 -2
- package/Samples~/DI - VContainer/Scripts/Spawner.cs +113 -4
- package/Samples~/DI - Zenject/README.md +223 -58
- package/Samples~/DI - Zenject/Scripts/RelationalConsumer.cs +3 -0
- package/Samples~/DI - Zenject/Scripts/RelationalConsumerPool.cs +37 -0
- package/Samples~/DI - Zenject/Scripts/RelationalConsumerPool.cs.meta +12 -0
- package/Samples~/DI - Zenject/Scripts/SpawnerZenject.cs +74 -3
- package/Samples~/Random - PRNG/README.md +2 -1
- package/Samples~/Relational Components - Basic/README.md +3 -1
- package/Samples~/Serialization - JSON/README.md +2 -1
- package/Samples~/Spatial Structures - 2D and 3D/README.md +2 -1
- package/Samples~/UGUI - EnhancedImage/README.md +2 -1
- package/Samples~/UI Toolkit - MultiFile Selector (Editor)/README.md +2 -1
- package/Tests/Editor/Attributes/AnimationEventHelpersTests.cs +16 -0
- package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +32 -34
- package/Tests/Editor/Integrations/Reflex/ReflexIntegrationCompilationTests.cs +41 -0
- package/Tests/Editor/Integrations/Reflex/ReflexIntegrationCompilationTests.cs.meta +11 -0
- package/Tests/Editor/Integrations/Reflex/WallstopStudios.UnityHelpers.Tests.Editor.Reflex.asmdef +27 -0
- package/Tests/Editor/Integrations/Reflex/WallstopStudios.UnityHelpers.Tests.Editor.Reflex.asmdef.meta +7 -0
- package/Tests/Editor/Integrations/Reflex.meta +8 -0
- package/Tests/Editor/Integrations/VContainer/VContainerRelationalEntryPointTests.cs +21 -18
- package/Tests/Editor/Integrations/VContainer/VContainerRelationalHelpersTests.cs +164 -0
- package/Tests/Editor/Integrations/VContainer/VContainerRelationalHelpersTests.cs.meta +11 -0
- package/Tests/Editor/Integrations/VContainer/WallstopStudios.UnityHelpers.Tests.Editor.VContainer.asmdef +2 -1
- package/Tests/Editor/Integrations/Zenject/WallstopStudios.UnityHelpers.Tests.Editor.Zenject.asmdef +3 -2
- package/Tests/Editor/Integrations/Zenject/ZenjectRelationalHelpersTests.cs +127 -0
- package/Tests/Editor/Integrations/Zenject/ZenjectRelationalHelpersTests.cs.meta +11 -0
- package/Tests/Editor/Integrations/Zenject/ZenjectRelationalInitializerTests.cs +25 -23
- package/Tests/Editor/PersistentDirectorySettingsTests.cs +58 -0
- package/Tests/Editor/PersistentDirectorySettingsTests.cs.meta +11 -0
- package/Tests/Editor/PrefabCheckerReportTests.cs +32 -0
- package/Tests/Editor/PrefabCheckerReportTests.cs.meta +11 -0
- package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs +63 -0
- package/Tests/Editor/Sprites/AnimationCopierFilterTests.cs.meta +11 -0
- package/Tests/Editor/Sprites/AnimationCopierWindowTests.cs +1 -1
- package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs +38 -0
- package/Tests/Editor/Sprites/AnimationViewerWindowTests.cs.meta +11 -0
- package/Tests/Editor/Sprites/ScriptableSpriteAtlasEditorTests.cs +1 -1
- package/Tests/Editor/Sprites/SpriteCropperAdditionalTests.cs +12 -12
- package/Tests/Editor/Sprites/SpriteCropperTests.cs +9 -9
- package/Tests/Editor/Sprites/SpritePivotAdjusterTests.cs +3 -3
- package/Tests/Editor/Sprites/TexturePlatformNameHelperTests.cs +18 -0
- package/Tests/Editor/Sprites/TextureResizerWizardTests.cs +5 -5
- package/Tests/Editor/Sprites/TextureSettingsApplierAPITests.cs +3 -3
- package/Tests/Editor/Sprites/TextureSettingsApplierWizardAdditionalTests.cs +4 -4
- package/Tests/Editor/Sprites/TextureSettingsApplierWizardTests.cs +4 -4
- package/Tests/Editor/Tools/ImageBlurToolTests.cs +22 -110
- package/Tests/Editor/Utils/CommonTestBase.cs +60 -1
- package/Tests/Editor/Utils/ScriptableObjectSingletonCreatorTests.cs +6 -6
- package/Tests/Editor/Windows/FitTextureSizeWindowTests.cs +66 -74
- package/Tests/Runtime/Attributes/RelationalComponentInitializerTests.cs +4 -15
- package/Tests/Runtime/DataStructures/SpatialTree3DBoundsConsistencyTests.cs +29 -29
- package/Tests/Runtime/Extensions/AsyncOperationExtensionsTests.cs +179 -0
- package/Tests/Runtime/Extensions/RandomExtensionTests.cs +55 -0
- package/Tests/Runtime/Integrations/Reflex/RelationalComponentsReflexTests.cs +445 -0
- package/Tests/Runtime/Integrations/Reflex/RelationalComponentsReflexTests.cs.meta +11 -0
- package/Tests/Runtime/Integrations/Reflex/WallstopStudios.UnityHelpers.Tests.Runtime.Reflex.asmdef +28 -0
- package/Tests/Runtime/Integrations/Reflex/WallstopStudios.UnityHelpers.Tests.Runtime.Reflex.asmdef.meta +7 -0
- package/Tests/Runtime/Integrations/Reflex.meta +8 -0
- package/Tests/Runtime/Integrations/VContainer/RelationalComponentsVContainerTests.cs +257 -221
- package/Tests/Runtime/Integrations/VContainer/RelationalObjectPoolsVContainerTests.cs +91 -0
- package/Tests/Runtime/Integrations/VContainer/RelationalObjectPoolsVContainerTests.cs.meta +11 -0
- package/Tests/Runtime/Integrations/Zenject/RelationalComponentsZenjectTests.cs +251 -233
- package/Tests/Runtime/Performance/RandomPerformanceTests.cs +1 -1
- package/Tests/Runtime/Performance/SpatialTree2DPerformanceTests.cs +6 -1
- package/Tests/Runtime/Performance/SpatialTree3DPerformanceTests.cs +4 -1
- package/Tests/Runtime/Serialization/JsonConverterAdditionalTests.cs +30 -0
- package/Tests/Runtime/Serialization/JsonConverterAdditionalTests.cs.meta +11 -0
- package/Tests/Runtime/Serialization/JsonConverterTests.cs +8 -12
- package/Tests/Runtime/Serialization/JsonRoundtripComprehensiveTests.cs +4 -9
- package/Tests/Runtime/Serialization/JsonSerializationTest.cs +16 -5
- package/Tests/Runtime/Serialization/ProtoRoundtripComprehensiveTests.cs +13 -13
- package/Tests/Runtime/Serialization/SerializerAdditionalTests.cs +12 -0
- package/Tests/Runtime/Serialization/SerializerFileIoTests.cs +105 -0
- package/Tests/Runtime/Serialization/SerializerFileIoTests.cs.meta +11 -0
- package/Tests/Runtime/Serialization/UnityEngineObjectJsonTests.cs +247 -0
- package/Tests/Runtime/Serialization/UnityEngineObjectJsonTests.cs.meta +11 -0
- package/Tests/Runtime/TestUtils/CommonTestBase.cs +99 -0
- package/Tests/Runtime/TestUtils/ReflexTestSupport.cs +111 -0
- package/Tests/Runtime/TestUtils/ReflexTestSupport.cs.meta +12 -0
- package/Tests/Runtime/Utils/CoroutineHandlerTests.cs +1 -1
- package/Tests/Runtime/Utils/LZMAComprehensiveTests.cs +1 -1
- package/Tests/Runtime/Utils/LZMATests.cs +1 -1
- package/Tests/Runtime/Utils/MatchColliderToSpriteTests.cs +5 -5
- package/Tests/Runtime/Visuals/EnhancedImageTests.cs +25 -56
- package/Tests/Runtime/Visuals/VisualsTestHelpers.cs +1 -8
- package/Tests/TestUtils.meta +8 -0
- package/package-lock.json.meta +7 -0
- package/package.json +13 -4
- package/scripts/check-eol.ps1 +4 -5
- package/scripts/lint-tests.ps1 +156 -0
- package/scripts/lint-tests.ps1.meta +7 -0
- package/scripts/normalize-eol.ps1 +6 -9
- package/.github/workflows/csharpier.yml +0 -135
- package/CHANGELOG.md +0 -0
- package/EFFECTS_SYSTEM.md +0 -242
- package/MATH_AND_EXTENSIONS.md +0 -316
- package/SPATIAL_TREE_2D_PERFORMANCE.md +0 -238
- package/SPATIAL_TREE_3D_PERFORMANCE.md +0 -240
- /package/{CHANGELOG.md.meta → Docs/CHANGELOG.md.meta} +0 -0
- /package/{DATA_STRUCTURES.md → Docs/DATA_STRUCTURES.md} +0 -0
- /package/{DATA_STRUCTURES.md.meta → Docs/DATA_STRUCTURES.md.meta} +0 -0
- /package/{EDITOR_TOOLS_GUIDE.md.meta → Docs/EDITOR_TOOLS_GUIDE.md.meta} +0 -0
- /package/{EFFECTS_SYSTEM.md.meta → Docs/EFFECTS_SYSTEM.md.meta} +0 -0
- /package/{EFFECTS_SYSTEM_TUTORIAL.md.meta → Docs/EFFECTS_SYSTEM_TUTORIAL.md.meta} +0 -0
- /package/{GETTING_STARTED.md.meta → Docs/GETTING_STARTED.md.meta} +0 -0
- /package/{GLOSSARY.md.meta → Docs/GLOSSARY.md.meta} +0 -0
- /package/{HULLS.md → Docs/HULLS.md} +0 -0
- /package/{HULLS.md.meta → Docs/HULLS.md.meta} +0 -0
- /package/{INDEX.md.meta → Docs/INDEX.md.meta} +0 -0
- /package/{LICENSE.md → Docs/LICENSE.md} +0 -0
- /package/{LICENSE.md.meta → Docs/LICENSE.md.meta} +0 -0
- /package/{MATH_AND_EXTENSIONS.md.meta → Docs/MATH_AND_EXTENSIONS.md.meta} +0 -0
- /package/{RANDOM_PERFORMANCE.md.meta → Docs/RANDOM_PERFORMANCE.md.meta} +0 -0
- /package/{REFLECTION_HELPERS.md → Docs/REFLECTION_HELPERS.md} +0 -0
- /package/{REFLECTION_HELPERS.md.meta → Docs/REFLECTION_HELPERS.md.meta} +0 -0
- /package/{RELATIONAL_COMPONENTS.md.meta → Docs/RELATIONAL_COMPONENTS.md.meta} +0 -0
- /package/{SERIALIZATION.md.meta → Docs/SERIALIZATION.md.meta} +0 -0
- /package/{SINGLETONS.md → Docs/SINGLETONS.md} +0 -0
- /package/{SINGLETONS.md.meta → Docs/SINGLETONS.md.meta} +0 -0
- /package/{SPATIAL_TREES_2D_GUIDE.md.meta → Docs/SPATIAL_TREES_2D_GUIDE.md.meta} +0 -0
- /package/{SPATIAL_TREES_3D_GUIDE.md.meta → Docs/SPATIAL_TREES_3D_GUIDE.md.meta} +0 -0
- /package/{SPATIAL_TREE_2D_PERFORMANCE.md.meta → Docs/SPATIAL_TREE_2D_PERFORMANCE.md.meta} +0 -0
- /package/{SPATIAL_TREE_3D_PERFORMANCE.md.meta → Docs/SPATIAL_TREE_3D_PERFORMANCE.md.meta} +0 -0
- /package/{SPATIAL_TREE_SEMANTICS.md → Docs/SPATIAL_TREE_SEMANTICS.md} +0 -0
- /package/{SPATIAL_TREE_SEMANTICS.md.meta → Docs/SPATIAL_TREE_SEMANTICS.md.meta} +0 -0
- /package/{THIRD_PARTY_NOTICES.md.meta → Docs/THIRD_PARTY_NOTICES.md.meta} +0 -0
package/.editorconfig
CHANGED
package/.gitattributes
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Line endings
|
|
3
3
|
#
|
|
4
4
|
# Enforce CRLF in the working tree for all text files to align with
|
|
5
|
-
# .editorconfig (end_of_line = crlf, charset = utf-8
|
|
5
|
+
# .editorconfig (end_of_line = crlf, charset = utf-8 without BOM) and avoid
|
|
6
6
|
# formatter diffs on Linux runners.
|
|
7
7
|
###############################################################################
|
|
8
8
|
* text=auto eol=crlf
|
package/.githooks/pre-commit
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# 0) Ensure .NET tools available (for CSharpier)
|
|
5
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
6
|
+
dotnet tool restore >/dev/null 2>&1 || true
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
# 1) Lint Markdown link text style (PowerShell script)
|
|
5
10
|
run_pwsh() {
|
|
6
11
|
pwsh -NoProfile -File scripts/lint-doc-links.ps1
|
|
7
12
|
}
|
|
@@ -19,7 +24,7 @@ else
|
|
|
19
24
|
exit 1
|
|
20
25
|
fi
|
|
21
26
|
|
|
22
|
-
# 2) Format staged Markdown/JSON
|
|
27
|
+
# 2) Format staged Markdown/JSON/YAML with Prettier and re-stage
|
|
23
28
|
if ! command -v npx >/dev/null 2>&1; then
|
|
24
29
|
echo "npx is required for formatting. Please install Node.js." >&2
|
|
25
30
|
exit 1
|
|
@@ -28,8 +33,9 @@ fi
|
|
|
28
33
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
29
34
|
|
|
30
35
|
# Collect file lists
|
|
31
|
-
MD_FILES=$(echo "$STAGED_FILES" | grep -E '
|
|
32
|
-
JSON_FILES=$(echo "$STAGED_FILES" | grep -E '
|
|
36
|
+
MD_FILES=$(echo "$STAGED_FILES" | grep -E '\\.(md|markdown)$' || true)
|
|
37
|
+
JSON_FILES=$(echo "$STAGED_FILES" | grep -E '\\.(json|asmdef|asmref)$' || true)
|
|
38
|
+
YAML_FILES=$(echo "$STAGED_FILES" | grep -E '\\.(yml|yaml)$' || true)
|
|
33
39
|
|
|
34
40
|
# Prettier format and re-add
|
|
35
41
|
if [ -n "$MD_FILES" ]; then
|
|
@@ -42,8 +48,28 @@ if [ -n "$JSON_FILES" ]; then
|
|
|
42
48
|
echo "$JSON_FILES" | xargs git add
|
|
43
49
|
fi
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
if [ -n "$YAML_FILES" ]; then
|
|
52
|
+
echo "$YAML_FILES" | xargs npx --no-install prettier --write --log-level warn
|
|
53
|
+
echo "$YAML_FILES" | xargs git add
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# 3) Format staged C# files with CSharpier (if available) and re-stage
|
|
57
|
+
CS_FILES=$(echo "$STAGED_FILES" | grep -E '\\.cs$' || true)
|
|
58
|
+
if [ -n "$CS_FILES" ] && command -v dotnet >/dev/null 2>&1; then
|
|
59
|
+
# Use local tool version to format paths; fall back to repo-wide if necessary
|
|
60
|
+
dotnet tool run csharpier $CS_FILES >/dev/null 2>&1 || dotnet tool run csharpier . >/dev/null 2>&1 || true
|
|
61
|
+
echo "$CS_FILES" | xargs git add
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 4) Markdown lint for staged Markdown files
|
|
46
65
|
if [ -n "$MD_FILES" ]; then
|
|
47
66
|
npx --no-install markdownlint $MD_FILES --config .markdownlint.json --ignore-path .markdownlintignore
|
|
48
67
|
fi
|
|
49
68
|
|
|
69
|
+
# 5) Optional YAML lint on staged YAML (if yamllint present)
|
|
70
|
+
if [ -n "$YAML_FILES" ]; then
|
|
71
|
+
if command -v yamllint >/dev/null 2>&1; then
|
|
72
|
+
echo "$YAML_FILES" | xargs yamllint -c .yamllint.yaml
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Mirror key CI checks before pushing
|
|
5
|
+
|
|
6
|
+
# Ensure Node is available
|
|
7
|
+
if ! command -v npx >/dev/null 2>&1; then
|
|
8
|
+
echo "npx is required. Please install Node.js." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# 1) Prettier checks (Markdown, JSON, asmdef/asmref, YAML)
|
|
13
|
+
npx --no-install prettier --check "**/*.{md,markdown}" || exit 1
|
|
14
|
+
npx --no-install prettier --check "**/*.{json,asmdef,asmref}" || exit 1
|
|
15
|
+
npx --no-install prettier --check "**/*.{yml,yaml}" || exit 1
|
|
16
|
+
|
|
17
|
+
# 2) Markdownlint (uses repo config and ignore list)
|
|
18
|
+
npx --no-install markdownlint "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore || exit 1
|
|
19
|
+
|
|
20
|
+
# 3) EOL policy (CRLF, no UTF-8 BOM)
|
|
21
|
+
if command -v pwsh >/dev/null 2>&1; then
|
|
22
|
+
pwsh -NoProfile -File scripts/check-eol.ps1 -VerboseOutput
|
|
23
|
+
elif command -v powershell >/dev/null 2>&1; then
|
|
24
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/check-eol.ps1 -VerboseOutput
|
|
25
|
+
else
|
|
26
|
+
echo "PowerShell not found; skipping EOL validation." >&2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 4) Optional YAML lint (if yamllint installed)
|
|
30
|
+
if command -v yamllint >/dev/null 2>&1; then
|
|
31
|
+
yamllint -c .yamllint.yaml .
|
|
32
|
+
else
|
|
33
|
+
echo "yamllint not found; skipping YAML lint. CI will catch issues." >&2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 5) Optional external link check (if lychee installed)
|
|
37
|
+
if command -v lychee >/dev/null 2>&1; then
|
|
38
|
+
lychee -c .lychee.toml --no-progress --verbose "./**/*.md"
|
|
39
|
+
else
|
|
40
|
+
echo "lychee not found; skipping external link check. CI will run lychee." >&2
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 6) Test lifecycle lint
|
|
44
|
+
if command -v pwsh >/dev/null 2>&1; then
|
|
45
|
+
pwsh -NoProfile -File scripts/lint-tests.ps1 -VerboseOutput
|
|
46
|
+
elif command -v powershell >/dev/null 2>&1; then
|
|
47
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/lint-tests.ps1 -VerboseOutput
|
|
48
|
+
else
|
|
49
|
+
echo 'PowerShell not found; skipping test lints.' >&2
|
|
50
|
+
fi
|
package/.github/dependabot.yml
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
updates:
|
|
3
|
+
# GitHub Actions workflow updates
|
|
3
4
|
- package-ecosystem: "github-actions"
|
|
4
5
|
directory: "/"
|
|
5
6
|
schedule:
|
|
6
|
-
interval: "
|
|
7
|
+
interval: "daily"
|
|
7
8
|
assignees:
|
|
8
9
|
- wallstop
|
|
9
10
|
reviewers:
|
|
10
|
-
- wallstop
|
|
11
|
+
- wallstop
|
|
12
|
+
|
|
13
|
+
# NuGet: .csproj/props/targets and .config/dotnet-tools.json (local tools)
|
|
14
|
+
- package-ecosystem: "nuget"
|
|
15
|
+
directory: "/"
|
|
16
|
+
schedule:
|
|
17
|
+
interval: "daily"
|
|
18
|
+
assignees:
|
|
19
|
+
- wallstop
|
|
20
|
+
reviewers:
|
|
21
|
+
- wallstop
|
|
22
|
+
|
|
23
|
+
# npm/UPM: package.json at repo root (Unity package manifest)
|
|
24
|
+
- package-ecosystem: "npm"
|
|
25
|
+
directory: "/"
|
|
26
|
+
schedule:
|
|
27
|
+
interval: "daily"
|
|
28
|
+
versioning-strategy: increase
|
|
29
|
+
assignees:
|
|
30
|
+
- wallstop
|
|
31
|
+
reviewers:
|
|
32
|
+
- wallstop
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Param(
|
|
2
|
+
[string]$Root = "."
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = 'Stop'
|
|
6
|
+
|
|
7
|
+
function Normalize-Name {
|
|
8
|
+
param([string]$s)
|
|
9
|
+
if ([string]::IsNullOrWhiteSpace($s)) { return "" }
|
|
10
|
+
# Remove extension (like .md), collapse non-alphanumerics, lowercase
|
|
11
|
+
$noExt = $s -replace '\.[^\.]+$',''
|
|
12
|
+
$normalized = ($noExt -replace '[^A-Za-z0-9]', '')
|
|
13
|
+
return $normalized.ToLowerInvariant()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
$issueCount = 0
|
|
17
|
+
|
|
18
|
+
# Exclude typical directories that shouldn't be scanned
|
|
19
|
+
$excludeDirs = @('.git', 'node_modules', '.vs')
|
|
20
|
+
|
|
21
|
+
$mdFiles = Get-ChildItem -Path $Root -Recurse -File -Filter *.md |
|
|
22
|
+
Where-Object { $excludeDirs -notcontains $_.Directory.Name }
|
|
23
|
+
|
|
24
|
+
# Regex for inline markdown links (exclude images), capture optional title
|
|
25
|
+
$pattern = '(?<!\!)\[(?<text>[^\]]+)\]\((?<target>[^)\s]+)(?:\s+"[^"]*")?\)'
|
|
26
|
+
|
|
27
|
+
foreach ($file in $mdFiles) {
|
|
28
|
+
$lines = Get-Content -LiteralPath $file.FullName -Encoding UTF8
|
|
29
|
+
for ($i = 0; $i -lt $lines.Count; $i++) {
|
|
30
|
+
$line = $lines[$i]
|
|
31
|
+
$matches = [System.Text.RegularExpressions.Regex]::Matches($line, $pattern)
|
|
32
|
+
foreach ($m in $matches) {
|
|
33
|
+
$text = $m.Groups['text'].Value.Trim()
|
|
34
|
+
$targetRaw = $m.Groups['target'].Value.Trim()
|
|
35
|
+
|
|
36
|
+
# Skip anchors, external links, and mailto
|
|
37
|
+
if ($targetRaw -match '^(#|https?://|mailto:|tel:|data:)') { continue }
|
|
38
|
+
|
|
39
|
+
# Remove query/anchor for file checks
|
|
40
|
+
$targetCore = $targetRaw -replace '[?#].*$',''
|
|
41
|
+
|
|
42
|
+
# Decode URL-encoded chars
|
|
43
|
+
try { $targetCore = [uri]::UnescapeDataString($targetCore) } catch { }
|
|
44
|
+
|
|
45
|
+
# Only care about links to markdown files
|
|
46
|
+
if (-not ($targetCore -match '\.md$')) { continue }
|
|
47
|
+
|
|
48
|
+
$fileName = [System.IO.Path]::GetFileName($targetCore)
|
|
49
|
+
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($targetCore)
|
|
50
|
+
|
|
51
|
+
# Fail when the visible link text is the raw file name
|
|
52
|
+
$isExactFileName = $text.Equals($fileName, [System.StringComparison]::OrdinalIgnoreCase)
|
|
53
|
+
|
|
54
|
+
# Also fail when the visible text looks like a path or ends with .md
|
|
55
|
+
# contains path separators and no whitespace (heuristic for raw paths)
|
|
56
|
+
$looksLikePath = ($text -match '[\\/]' -and -not ($text -match '\\s'))
|
|
57
|
+
$looksLikeMarkdownFileName = $text.Trim().ToLowerInvariant().EndsWith('.md')
|
|
58
|
+
|
|
59
|
+
if ($isExactFileName -or $looksLikePath -or $looksLikeMarkdownFileName) {
|
|
60
|
+
$issueCount++
|
|
61
|
+
$lineNo = $i + 1
|
|
62
|
+
$msg = "Link text '$text' should be human-readable, not a raw file name or path"
|
|
63
|
+
# GitHub Actions annotation
|
|
64
|
+
Write-Output "::error file=$($file.FullName),line=$lineNo::$msg (target: $targetRaw)"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ($issueCount -gt 0) {
|
|
71
|
+
Write-Host "Found $issueCount documentation link(s) with non-human-readable text." -ForegroundColor Red
|
|
72
|
+
Write-Host "Use a descriptive phrase instead of the raw file name."
|
|
73
|
+
exit 1
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
Write-Host "All markdown links have human-readable text."
|
|
77
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
EXCLUDE_DIRS = {".git", "node_modules", ".vs"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_name(s: str) -> str:
|
|
12
|
+
if not s:
|
|
13
|
+
return ""
|
|
14
|
+
# remove extension, strip non-alphanumerics, lowercase
|
|
15
|
+
base = re.sub(r"\.[^.]+$", "", s)
|
|
16
|
+
return re.sub(r"[^A-Za-z0-9]", "", base).lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
LINK_RE = re.compile(r"(?<!\!)\[(?P<text>[^\]]+)\]\((?P<target>[^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def should_check_target(target: str) -> bool:
|
|
23
|
+
if re.match(r"^(#|https?://|mailto:|tel:|data:)", target):
|
|
24
|
+
return False
|
|
25
|
+
# only check links that end in .md (ignoring anchors/query)
|
|
26
|
+
core = re.sub(r"[?#].*$", "", target)
|
|
27
|
+
try:
|
|
28
|
+
core = urllib.parse.unquote(core)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
return core.lower().endswith(".md")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(root: str) -> int:
|
|
35
|
+
issues = 0
|
|
36
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
37
|
+
# prune excluded directories
|
|
38
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
39
|
+
for filename in filenames:
|
|
40
|
+
if not filename.lower().endswith(".md"):
|
|
41
|
+
continue
|
|
42
|
+
path = os.path.join(dirpath, filename)
|
|
43
|
+
try:
|
|
44
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
45
|
+
lines = f.readlines()
|
|
46
|
+
except Exception:
|
|
47
|
+
continue
|
|
48
|
+
for idx, line in enumerate(lines, start=1):
|
|
49
|
+
for m in LINK_RE.finditer(line):
|
|
50
|
+
text = m.group("text").strip()
|
|
51
|
+
target_raw = m.group("target").strip()
|
|
52
|
+
if not should_check_target(target_raw):
|
|
53
|
+
continue
|
|
54
|
+
target_core = re.sub(r"[?#].*$", "", target_raw)
|
|
55
|
+
try:
|
|
56
|
+
target_core = urllib.parse.unquote(target_core)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
file_name = os.path.basename(target_core)
|
|
60
|
+
base_name, _ = os.path.splitext(file_name)
|
|
61
|
+
|
|
62
|
+
is_exact_file_name = text.lower() == file_name.lower()
|
|
63
|
+
looks_like_path = (("/" in text) or ("\\" in text)) and not re.search(r"\s", text)
|
|
64
|
+
looks_like_markdown = text.strip().lower().endswith(".md")
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
is_exact_file_name
|
|
68
|
+
or looks_like_path
|
|
69
|
+
or looks_like_markdown
|
|
70
|
+
):
|
|
71
|
+
issues += 1
|
|
72
|
+
msg = f"{path}:{idx}: Link text '{text}' should be human-readable, not a raw file name or path (target: {target_raw})"
|
|
73
|
+
print(msg)
|
|
74
|
+
|
|
75
|
+
if issues:
|
|
76
|
+
print(
|
|
77
|
+
f"Found {issues} documentation link(s) with non-human-readable text.",
|
|
78
|
+
file=sys.stderr,
|
|
79
|
+
)
|
|
80
|
+
print(
|
|
81
|
+
"Use a descriptive phrase instead of the raw file name.", file=sys.stderr
|
|
82
|
+
)
|
|
83
|
+
return 1
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
root = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
89
|
+
sys.exit(main(root))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
EXCLUDE_DIRS = {".git", "node_modules", ".vs", ".vscode", "Library", "Temp"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Inline markdown link or image:  or [text](target "title")
|
|
11
|
+
INLINE_LINK_RE = re.compile(
|
|
12
|
+
r"!?(?P<all>\[(?P<text>[^\]]+)\]\((?P<target>[^)\s]+)(?:\s+\"[^\"]*\")?\))"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Reference-style link definitions: [id]: target "title"
|
|
16
|
+
REF_DEF_RE = re.compile(r"^\s*\[[^\]]+\]:\s*(?P<target>\S+)(?:\s+\"[^\"]*\")?\s*$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_external(target: str) -> bool:
|
|
20
|
+
return target.startswith("http://") or target.startswith("https://") or target.startswith("mailto:") or target.startswith("tel:") or target.startswith("data:")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def has_unencoded_chars(target: str) -> bool:
|
|
24
|
+
# Only flag raw spaces or plus signs in the path/query/fragment
|
|
25
|
+
return (" " in target) or ("+" in target)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scan_file(path: str) -> int:
|
|
29
|
+
issues = 0
|
|
30
|
+
try:
|
|
31
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
32
|
+
lines = f.readlines()
|
|
33
|
+
except Exception:
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
for idx, line in enumerate(lines, start=1):
|
|
37
|
+
# Inline links/images
|
|
38
|
+
for m in INLINE_LINK_RE.finditer(line):
|
|
39
|
+
target = m.group("target").strip()
|
|
40
|
+
if is_external(target):
|
|
41
|
+
continue
|
|
42
|
+
if has_unencoded_chars(target):
|
|
43
|
+
issues += 1
|
|
44
|
+
print(f"{path}:{idx}: Unencoded character(s) in link target: '{target}'. Encode spaces as %20 and '+' as %2B.")
|
|
45
|
+
|
|
46
|
+
# Reference-style link definitions
|
|
47
|
+
m = REF_DEF_RE.match(line)
|
|
48
|
+
if m:
|
|
49
|
+
target = m.group("target").strip()
|
|
50
|
+
if not is_external(target) and has_unencoded_chars(target):
|
|
51
|
+
issues += 1
|
|
52
|
+
print(f"{path}:{idx}: Unencoded character(s) in link definition: '{target}'. Encode spaces as %20 and '+' as %2B.")
|
|
53
|
+
|
|
54
|
+
return issues
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(root: str) -> int:
|
|
58
|
+
issues = 0
|
|
59
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
60
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
61
|
+
for filename in filenames:
|
|
62
|
+
if filename.lower().endswith(".md"):
|
|
63
|
+
issues += scan_file(os.path.join(dirpath, filename))
|
|
64
|
+
if issues:
|
|
65
|
+
print(f"Found {issues} markdown link(s) with unencoded spaces or plus signs.", file=sys.stderr)
|
|
66
|
+
print("Please URL-encode spaces as %20 and '+' as %2B in relative links.", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
root = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
73
|
+
sys.exit(main(root))
|
|
74
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from typing import Dict, List, Set, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
EXCLUDE_DIRS = {".git", "node_modules", ".vs"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LINK_RE = re.compile(r"(?<!\!)\[(?P<text>[^\]]+)\]\((?P<target>[^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def unescape_uri(s: str) -> str:
|
|
16
|
+
try:
|
|
17
|
+
return urllib.parse.unquote(s)
|
|
18
|
+
except Exception:
|
|
19
|
+
return s
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normalize_heading_to_id(text: str) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Approximate GitHub-style anchor slug generation (GFM):
|
|
25
|
+
- Lowercase
|
|
26
|
+
- Strip Markdown formatting and code ticks
|
|
27
|
+
- Remove most punctuation
|
|
28
|
+
- Replace whitespace with hyphens
|
|
29
|
+
- Collapse multiple hyphens
|
|
30
|
+
- Trim leading/trailing hyphens
|
|
31
|
+
"""
|
|
32
|
+
if not text:
|
|
33
|
+
return ""
|
|
34
|
+
t = text
|
|
35
|
+
# Remove inline code ticks
|
|
36
|
+
t = t.replace("`", "")
|
|
37
|
+
# Remove Markdown emphasis markers
|
|
38
|
+
t = t.replace("*", "").replace("_", "").replace("~", "")
|
|
39
|
+
# Remove link/image markup inside headings e.g. [text](url)
|
|
40
|
+
t = re.sub(r"!?\[[^\]]*\]\([^)]*\)", "", t)
|
|
41
|
+
# Remove HTML tags
|
|
42
|
+
t = re.sub(r"<[^>]+>", "", t)
|
|
43
|
+
# Lowercase
|
|
44
|
+
t = t.lower()
|
|
45
|
+
# Replace whitespace with hyphens
|
|
46
|
+
t = re.sub(r"\s+", "-", t)
|
|
47
|
+
# Remove punctuation except hyphens and alphanumerics
|
|
48
|
+
t = re.sub(r"[^a-z0-9-]", "", t)
|
|
49
|
+
# Collapse multiple hyphens
|
|
50
|
+
t = re.sub(r"-+", "-", t)
|
|
51
|
+
# Trim hyphens
|
|
52
|
+
t = t.strip("-")
|
|
53
|
+
return t
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def collect_heading_ids(file_path: str) -> Set[str]:
|
|
57
|
+
"""
|
|
58
|
+
Build the set of anchor IDs generated by headings within a markdown file.
|
|
59
|
+
Handles ATX (# ...) and Setext (underlined) headings. Accounts for duplicate
|
|
60
|
+
slugs by adding -1, -2, ... suffixes (GitHub behavior).
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
64
|
+
lines = f.readlines()
|
|
65
|
+
except Exception:
|
|
66
|
+
return set()
|
|
67
|
+
|
|
68
|
+
ids: Set[str] = set()
|
|
69
|
+
slug_counts: Dict[str, int] = {}
|
|
70
|
+
|
|
71
|
+
def add_slug_from_text(text: str):
|
|
72
|
+
slug = normalize_heading_to_id(text)
|
|
73
|
+
if slug == "":
|
|
74
|
+
return
|
|
75
|
+
count = slug_counts.get(slug, 0)
|
|
76
|
+
if count == 0:
|
|
77
|
+
final = slug
|
|
78
|
+
else:
|
|
79
|
+
final = f"{slug}-{count}"
|
|
80
|
+
slug_counts[slug] = count + 1
|
|
81
|
+
ids.add(final)
|
|
82
|
+
|
|
83
|
+
# ATX headings
|
|
84
|
+
atx_re = re.compile(r"^\s{0,3}#{1,6}\s+(.*)$")
|
|
85
|
+
|
|
86
|
+
# Walk lines, handle setext headings by looking ahead
|
|
87
|
+
i = 0
|
|
88
|
+
while i < len(lines):
|
|
89
|
+
line = lines[i].rstrip("\n")
|
|
90
|
+
m = atx_re.match(line)
|
|
91
|
+
if m:
|
|
92
|
+
add_slug_from_text(m.group(1).strip())
|
|
93
|
+
i += 1
|
|
94
|
+
continue
|
|
95
|
+
# Setext H1/H2
|
|
96
|
+
if i + 1 < len(lines):
|
|
97
|
+
underline = lines[i + 1].rstrip("\n")
|
|
98
|
+
if re.match(r"^\s{0,3}=+\s*$", underline) or re.match(r"^\s{0,3}-+\s*$", underline):
|
|
99
|
+
add_slug_from_text(line.strip())
|
|
100
|
+
i += 2
|
|
101
|
+
continue
|
|
102
|
+
i += 1
|
|
103
|
+
|
|
104
|
+
return ids
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_external(target: str) -> bool:
|
|
108
|
+
return bool(re.match(r"^(https?://|mailto:|tel:|data:)", target))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_path(base_dir: str, target_path: str) -> str:
|
|
112
|
+
return os.path.normpath(os.path.join(base_dir, target_path))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_internal_link(src_file: str, target_raw: str) -> Tuple[bool, str]:
|
|
116
|
+
# Separate fragment
|
|
117
|
+
if target_raw.startswith("#"):
|
|
118
|
+
# Anchor within same file
|
|
119
|
+
frag = target_raw[1:]
|
|
120
|
+
anchor = unescape_uri(frag)
|
|
121
|
+
anchor = anchor.strip()
|
|
122
|
+
anchor = anchor.lower()
|
|
123
|
+
ids = collect_heading_ids(src_file)
|
|
124
|
+
if anchor in ids:
|
|
125
|
+
return True, ""
|
|
126
|
+
return False, f"dangling anchor '#{frag}' (no matching heading)"
|
|
127
|
+
|
|
128
|
+
# Split off query/fragment
|
|
129
|
+
core = re.sub(r"[?#].*$", "", target_raw)
|
|
130
|
+
core = unescape_uri(core)
|
|
131
|
+
base_dir = os.path.dirname(src_file)
|
|
132
|
+
target_fs = resolve_path(base_dir, core)
|
|
133
|
+
if not os.path.exists(target_fs):
|
|
134
|
+
return False, f"target file not found: {core}"
|
|
135
|
+
|
|
136
|
+
# Fragment check if present
|
|
137
|
+
m = re.search(r"#(.+)$", target_raw)
|
|
138
|
+
if m:
|
|
139
|
+
frag = m.group(1)
|
|
140
|
+
anchor = unescape_uri(frag).strip().lower()
|
|
141
|
+
ids = collect_heading_ids(target_fs)
|
|
142
|
+
if anchor not in ids:
|
|
143
|
+
return False, f"dangling anchor '#{frag}' in {core}"
|
|
144
|
+
|
|
145
|
+
return True, ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(paths: List[str]) -> int:
|
|
149
|
+
issues = 0
|
|
150
|
+
|
|
151
|
+
def iter_markdown_files() -> List[str]:
|
|
152
|
+
files: List[str] = []
|
|
153
|
+
for p in paths:
|
|
154
|
+
if os.path.isdir(p):
|
|
155
|
+
for dirpath, dirnames, filenames in os.walk(p):
|
|
156
|
+
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
157
|
+
for filename in filenames:
|
|
158
|
+
if filename.lower().endswith(".md"):
|
|
159
|
+
files.append(os.path.join(dirpath, filename))
|
|
160
|
+
else:
|
|
161
|
+
if p.lower().endswith(".md") and os.path.exists(p):
|
|
162
|
+
files.append(p)
|
|
163
|
+
return files
|
|
164
|
+
|
|
165
|
+
files = iter_markdown_files()
|
|
166
|
+
for path in files:
|
|
167
|
+
try:
|
|
168
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
169
|
+
lines = f.readlines()
|
|
170
|
+
except Exception:
|
|
171
|
+
continue
|
|
172
|
+
for idx, line in enumerate(lines, start=1):
|
|
173
|
+
for m in LINK_RE.finditer(line):
|
|
174
|
+
target = m.group("target").strip()
|
|
175
|
+
# Skip images, external links, anchors we can't resolve externally here
|
|
176
|
+
if is_external(target):
|
|
177
|
+
continue
|
|
178
|
+
ok, reason = check_internal_link(path, target)
|
|
179
|
+
if not ok:
|
|
180
|
+
issues += 1
|
|
181
|
+
print(f"{path}:{idx}: Broken link '{target}': {reason}")
|
|
182
|
+
|
|
183
|
+
if issues:
|
|
184
|
+
print(f"Found {issues} broken internal markdown link(s).", file=sys.stderr)
|
|
185
|
+
print("Fix the paths or anchors so links resolve.", file=sys.stderr)
|
|
186
|
+
return 1
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
args = sys.argv[1:]
|
|
192
|
+
if not args:
|
|
193
|
+
args = ["."]
|
|
194
|
+
sys.exit(main(args))
|