diff --git a/.github/workflows/sync-brightscript-grammar.yml b/.github/workflows/sync-brightscript-grammar.yml new file mode 100644 index 0000000..827272d --- /dev/null +++ b/.github/workflows/sync-brightscript-grammar.yml @@ -0,0 +1,70 @@ +name: Sync BrightScript grammar + +# Renovate watches dev-docs/src/syntaxes/.version-info (see renovate.json +# customManagers) and opens a PR bumping the `ref:` line when a new release +# of rokucommunity/vscode-brightscript-language is published. This workflow +# fires on that PR, re-fetches the grammar JSON from the new tag, and +# commits the updated file to the PR branch — so a single review/merge +# updates both files atomically. +# +# Also runnable manually via workflow_dispatch in case the auto-commit +# misses (e.g. fork PRs, where GITHUB_TOKEN can't push to the head branch). + +on: + pull_request: + paths: + - 'dev-docs/src/syntaxes/.version-info' + workflow_dispatch: + inputs: + ref: + description: 'Upstream tag (e.g. v2.65.0). Defaults to whatever .version-info says.' + required: false + type: string + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + # Skip on PRs from forks — GITHUB_TOKEN doesn't have push perms there. + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref || github.ref }} + # PAT or a github-app token would also work; default GITHUB_TOKEN + # is sufficient since we're only pushing to the same repo. + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read upstream ref + id: ref + run: | + if [ -n "${{ inputs.ref }}" ]; then + echo "value=${{ inputs.ref }}" >> "$GITHUB_OUTPUT" + else + VALUE=$(awk '/^ref:/ {print $2}' dev-docs/src/syntaxes/.version-info) + if [ -z "$VALUE" ]; then + echo "::error::Could not read ref: from .version-info" + exit 1 + fi + echo "value=$VALUE" >> "$GITHUB_OUTPUT" + fi + + - name: Fetch grammar from upstream + run: | + curl -fsSL \ + "https://raw.githubusercontent.com/rokucommunity/vscode-brightscript-language/${{ steps.ref.outputs.value }}/syntaxes/brightscript.tmLanguage.json" \ + -o dev-docs/src/syntaxes/brightscript.tmLanguage.json + + - name: Commit if changed + run: | + if git diff --quiet dev-docs/src/syntaxes/brightscript.tmLanguage.json; then + echo "Grammar already matches ${{ steps.ref.outputs.value }} — nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add dev-docs/src/syntaxes/brightscript.tmLanguage.json + git commit -m "sync: brightscript grammar to ${{ steps.ref.outputs.value }}" + git push diff --git a/.prettierignore b/.prettierignore index 669efb4..4b2c00e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,6 @@ node_modules .jellyrock-repo .shared-ui-repo package-lock.json + +# Vendored upstream grammars — preserve original formatting +**/src/syntaxes/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a0541e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "// files.associations": "Stop the markdownlint extension from flagging dotfiles whose first line happens to start with '#'. None of these are markdown.", + "files.associations": { + ".prettierignore": "ignore", + ".version-info": "yaml" + } +} diff --git a/dev-docs/astro.config.mjs b/dev-docs/astro.config.mjs index 67b51d0..e0dd26a 100644 --- a/dev-docs/astro.config.mjs +++ b/dev-docs/astro.config.mjs @@ -1,4 +1,5 @@ // @ts-check +import { readFileSync } from 'node:fs'; import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import remarkGithubAlerts from 'remark-github-alerts'; @@ -9,6 +10,13 @@ import { fetchSharedUi } from '../shared/fetch-shared-ui.mjs'; await fetchSharedUi(); await fetchDocs('dev'); +// BrightScript TextMate grammar vendored from rokucommunity (MIT). Version +// tracked in src/syntaxes/.version-info; sync-brightscript-grammar workflow +// re-fetches when Renovate bumps the ref. +const brightscriptGrammar = JSON.parse( + readFileSync(new URL('./src/syntaxes/brightscript.tmLanguage.json', import.meta.url), 'utf8') +); + // https://astro.build/config export default defineConfig({ site: 'https://dev.jellyrock.app', @@ -17,6 +25,15 @@ export default defineConfig({ title: 'JellyRock Dev Guide', description: 'Developer documentation for JellyRock, a Jellyfin client for Roku.', favicon: '/favicon.png', + expressiveCode: { + shiki: { + langs: [brightscriptGrammar], + langAlias: { + brighterscript: 'brightscript', + bs: 'brightscript', + }, + }, + }, head: [ { tag: 'script', diff --git a/dev-docs/src/syntaxes/.version-info b/dev-docs/src/syntaxes/.version-info new file mode 100644 index 0000000..e52d393 --- /dev/null +++ b/dev-docs/src/syntaxes/.version-info @@ -0,0 +1,11 @@ +# Tracks the upstream source of brightscript.tmLanguage.json. Renovate watches +# this file (see renovate.json customManagers) and opens a PR when a newer +# release of rokucommunity/vscode-brightscript-language is published. The PR +# trigger fires the sync-brightscript-grammar workflow, which re-fetches the +# grammar JSON from the new tag and commits the updated file to the PR branch. +# Result: a single review/merge updates both the version ref AND the grammar. +# +# DO NOT EDIT the ref manually unless you also re-fetch the grammar JSON. + +# renovate: datasource=github-releases depName=rokucommunity/vscode-brightscript-language +ref: v2.65.0 \ No newline at end of file diff --git a/dev-docs/src/syntaxes/brightscript.tmLanguage.json b/dev-docs/src/syntaxes/brightscript.tmLanguage.json new file mode 100644 index 0000000..ea371cb --- /dev/null +++ b/dev-docs/src/syntaxes/brightscript.tmLanguage.json @@ -0,0 +1,1124 @@ +{ + "name": "BrightScript", + "scopeName": "source.brs", + "fileTypes": [ + "bs", + "brs" + ], + "uuid": "3194ae45-03ed-40d0-97fa-55bf088ceabb", + "foldingStartMarker": "(?i)^\\s*(sub|if|f(or( each)?|unction)|while)\\s*([a-zA-Z_]\\w*)\\s*(\\(.*\\)\\s*)?$", + "foldingStopMarker": "(?i)^\\s*(next|e(nd(if| (sub|if|function))|xit( while)?))\\s*$", + "patterns": [ + { + "include": "#entire_language" + } + ], + "repository": { + "entire_language": { + "patterns": [ + { + "include": "#regex" + }, + { + "include": "#bsdoc" + }, + { + "include": "#if_with_paren" + }, + { + "include": "#component_statement" + }, + { + "include": "#apostrophe_comment" + }, + { + "include": "#template_string" + }, + { + "include": "#rem_comment" + }, + { + "include": "#import_statement" + }, + { + "include": "#alias_statement" + }, + { + "include": "#namespace_declaration" + }, + { + "include": "#enum_declaration" + }, + { + "include": "#end_namespace" + }, + { + "include": "#method" + }, + { + "include": "#field" + }, + { + "include": "#class_declaration" + }, + { + "include": "#end_class" + }, + { + "include": "#preprocessor_keywords" + }, + { + "include": "#region_comment" + }, + { + "include": "#end_region_comment" + }, + { + "include": "#global_constants" + }, + { + "include": "#keyword_logical_operator" + }, + { + "include": "#function_call" + }, + { + "include": "#numeric_literal" + }, + { + "include": "#object_properties" + }, + { + "include": "#vscode_rale_tracker_entry_comment" + }, + { + "include": "#identifiers_with_type_designators" + }, + { + "include": "#m_and_global" + }, + { + "include": "#keyword_return" + }, + { + "include": "#primitive_literal_expression" + }, + { + "include": "#function_declaration" + }, + { + "include": "#inline_function_declaration" + }, + { + "include": "#end_function" + }, + { + "include": "#interface_declaration" + }, + { + "include": "#function_param_ish" + }, + { + "include": "#storage_types" + }, + { + "include": "#loop_keywords" + }, + { + "include": "#program_statements" + }, + { + "include": "#try_catch" + }, + { + "include": "#non_identifier_keywords" + }, + { + "include": "#operators" + }, + { + "include": "#variables_and_params" + }, + { + "include": "#annotation" + } + ] + }, + "regex": { + "patterns": [ + { + "name": "string.regexp.brs", + "begin": "(?|&&|\\|\\||\\*\\/)\\s*(\\/)(?![\\/*])(?=(?:[^\\/\\\\\\[\\()]|\\\\.|\\[([^\\]\\\\]|\\\\.)+\\]|\\(([^\\)\\\\]|\\\\.)+\\))+\\/([gmixsuXUAJ]+|(?![\\/\\*])|(?=\\/\\*))(?!\\s*[a-zA-Z0-9_$]))", + "beginCaptures": { + "1": { + "name": "punctuation.definition.string.begin.brs" + } + }, + "end": "(/)([gmixsuXUAJ]*)", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.brs" + }, + "2": { + "name": "keyword.other.brs" + } + }, + "patterns": [ + { + "include": "#regexp" + } + ] + }, + { + "name": "string.regexp.brs", + "begin": "((?", + "captures": { + "0": { + "name": "keyword.other.back-reference.regexp" + }, + "1": { + "name": "variable.other.regexp" + } + } + }, + { + "name": "keyword.operator.quantifier.regexp", + "match": "[?+*]|\\{(\\d+,\\d+|\\d+,|,\\d+|\\d+)\\}\\??" + }, + { + "name": "keyword.operator.or.regexp", + "match": "\\|" + }, + { + "name": "meta.group.assertion.regexp", + "begin": "(\\()((\\?=)|(\\?!)|(\\?\\|)|(\\?<=)|(\\?])|(?:\\?<([a-zA-Z_$][\\w$]*)>))?", + "beginCaptures": { + "0": { + "name": "punctuation.definition.group.regexp" + }, + "1": { + "name": "punctuation.definition.group.no-capture.regexp" + }, + "2": { + "name": "variable.other.regexp" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.definition.group.regexp" + } + }, + "patterns": [ + { + "include": "#regexp" + } + ] + }, + { + "name": "constant.other.character-class.set.regexp", + "begin": "(\\[)(\\^)?", + "beginCaptures": { + "1": { + "name": "punctuation.definition.character-class.regexp" + }, + "2": { + "name": "keyword.operator.negation.regexp" + } + }, + "end": "(\\])", + "endCaptures": { + "1": { + "name": "punctuation.definition.character-class.regexp" + } + }, + "patterns": [ + { + "name": "constant.other.character-class.range.regexp", + "match": "(?:.|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))\\-(?:[^\\]\\\\]|(\\\\(?:[0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))|(\\\\c[A-Z])|(\\\\.))", + "captures": { + "1": { + "name": "constant.character.numeric.regexp" + }, + "2": { + "name": "constant.character.control.regexp" + }, + "3": { + "name": "constant.character.escape.backslash.regexp" + }, + "4": { + "name": "constant.character.numeric.regexp" + }, + "5": { + "name": "constant.character.control.regexp" + }, + "6": { + "name": "constant.character.escape.backslash.regexp" + } + } + }, + { + "include": "#regex-character-class" + } + ] + }, + { + "include": "#regex-character-class" + } + ] + }, + "regex-character-class": { + "patterns": [ + { + "name": "constant.other.character-class.regexp", + "match": "\\\\[wWsSdDtrnvf]|\\." + }, + { + "name": "constant.character.numeric.regexp", + "match": "\\\\([0-7]{3}|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})" + }, + { + "name": "constant.character.control.regexp", + "match": "\\\\c[A-Z]" + }, + { + "name": "constant.character.escape.backslash.regexp", + "match": "\\\\." + } + ] + }, + "template_string": { + "begin": "(`)", + "beginCaptures": { + "1": { + "name": "string.template.brs" + } + }, + "patterns": [ + { + "begin": "(\\$\\{)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.template-expression.begin.brs" + } + }, + "patterns": [ + { + "include": "#entire_language" + } + ], + "end": "(\\})", + "endCaptures": { + "1": { + "name": "punctuation.definition.template-expression.end" + } + } + }, + { + "match": "(.)", + "name": "string.template.brs" + } + ], + "end": "(`)", + "endCaptures": { + "1": { + "name": "string.template.brs" + } + } + }, + "import_statement": { + "match": "(?i:(import)\\s*(\".*\"))", + "captures": { + "1": { + "name": "keyword.control.import.brs" + }, + "2": { + "name": "string.quoted.double.brs" + } + } + }, + "alias_statement": { + "match": "(?i:(alias)\\s+([a-z0-9_]+)\\s*(=)\\s*)", + "captures": { + "1": { + "name": "keyword.declaration.alias.brs" + }, + "2": { + "name": "entity.name.variable.brs" + }, + "3": { + "name": "keyword.operator.assignment.brs" + } + } + }, + "namespace_declaration": { + "begin": "(?i:(namespace))\\s+", + "beginCaptures": { + "1": { + "name": "keyword.other.namespace.brs" + } + }, + "end": "[\\s'\\n]", + "patterns": [ + { + "name": "entity.name.type.namespace.brs", + "match": "(?i:([a-z0-9_]+))" + }, + { + "name": "punctuation.accessor.brs", + "match": "\\." + } + ] + }, + "end_namespace": { + "match": "^(?i:\\s*(end\\s*namespace)\\s*(?=['\\n]))", + "captures": { + "1": { + "name": "keyword.other.namespace.brs" + } + } + }, + "component_statement": { + "name": "meta.component.brs", + "begin": "(?i)^[ \t]*(component)\\s+(?:([a-z0-9_]+)|(\".*?\"))(?:\\s+(extends)(?:\\s+(?:([a-z0-9_]+)|(\".*?\")))?)?\\s*", + "beginCaptures": { + "1": { + "name": "storage.type.component.brs" + }, + "2": { + "name": "entity.name.type.component.brs" + }, + "3": { + "name": "string.quoted.double.brs" + }, + "4": { + "name": "storage.modifier.extends.brs" + }, + "5": { + "name": "entity.name.type.component.brs" + }, + "6": { + "name": "string.quoted.double.brs" + } + }, + "end": "(?i)(?=||<|<>|\\+|-|\\*|\\/|\\^|&|\\b(?i:(And|Not|Or|Mod))\\b", + "name": "keyword.operator.brs" + }, + "global_constants": { + "match": "(?i:\\b(line_num)\\b)", + "name": "variable.language" + }, + "class_roku_builtin": { + "match": "(?i:(?<=\")(roAppInfo|roAppManager|roAppMemoryMonitor|roAppMemoryMonitorEvent|roAppendFile|roArray|roAssetCollection|roAssetFetcher|roAssetFetcherEvent|roAssetFetcherProgressEvent|roAssetPool|roAssetPoolFiles|roAssetRealizer|roAssetRealizerEvent|roAssociativeArray|roAudioConfiguration|roAudioEvent|roAudioEventMx|roAudioGuide|roAudioMetadata|roAudioOutput|roAudioPlayer|roAudioPlayerEvent|roAudioPlayerMx|roAudioResource|roBitmap|roBlockCipher|roBoolean|roBrSub|roBrightPackage|roBtClient|roBtClientEvent|roBtClientManager|roBtClientManagerEvent|roBtManager|roByteArray|roCECStatus|roCECStatusEvent|roCanvasWidget|roCaptionRenderer|roCaptionRendererEvent|roCecInterface|roCecRxFrameEvent|roCecTxCompleteEvent|roChannelManager|roChannelStore|roChannelStoreEvent|roCharDisplay|roClockWidget|roCodeRegistrationScreen|roCodeRegistrationScreenEvent|roCompositor|roConfigurationElements|roControlDown|roControlPort|roControlUp|roCreateFile|roDataGramSocket|roDatagramEvent|roDatagramReceiver|roDatagramSender|roDatagramSocket|roDateTime|roDeviceCrypto|roDeviceCustomization|roDeviceInfo|roDeviceInfoEvent|roDiskErrorEvent|roDiskMonitor|roDouble|roDsa|roEVPCipher|roEVPDigest|roElectron|roElectronEvent|roFileSystem|roFileSystemEvent|roFloat|roFont|roFontMetrics|roFontRegistry|roFunction|roGPIOButton|roGPIOControlPort|roGlobal|roGpioButton|roGpioControlPort|roGridScreen|roGridScreenEvent|roHMAC|roHashGenerator|roHdmiHotPlugEvent|roHdmiInputChanged|roHdmiOutputChanged|roHdmiStatus|roHdmiStatusEvent|roHtmlWidget|roHtmlWidgetEvent|roHttpAgent|roHttpEvent|roHttpServer|roIRDownEvent|roIRRepeatEvent|roIRUpEvent|roIRReceiver|roIRRemote|roIRRemotePress|roIRTransmitCompleteEvent|roIRTransmitter|roImageBuffer|roImageCanvas|roImageCanvasEvent|roImageMetaData|roImageMetadata|roImagePlayer|roImageWidget|roInput|roInputEvent|roInt|roIntrinsicDouble|roInvalid|roJRE|roKeyStore|roKeyboard|roKeyboardPress|roKeyboardScreen|roKeyboardScreenEvent|roList|roListScreen|roListScreenEvent|roLocalization|roLongInteger|roMediaServer|roMediaStreamer|roMediaStreamerEvent|roMessageDialog|roMessageDialogEvent|roMessagePort|roMicrophone|roMicrophoneEvent|roMimeStream|roMimeStreamEvent|roNetworkAdvertisement|roNetworkAttached|roNetworkConfiguration|roNetworkDetached|roNetworkDiscovery|roNetworkHotplug|roNetworkStatistics|roNetworkTimeEvent|roNodeJs|roNodeJsEvent|roOneLineDialog|roOneLineDialogEvent|roOpenVpn|roParagraphScreen|roParagraphScreenEvent|roPassKey|roPath|roPinEntryDialog|roPinEntryDialogEvent|roPosterScreen|roPosterScreenEvent|roPowerEvent|roPowerManager|roProgramGuide|roPtp|roPtpEvent|roQuadravoxButton|roQuadravoxSNS5|roRSA|roRSSArticle|roRSSParser|roReadFile|roReadWriteFile|roRectangle|roRegex|roRegion|roRegistry|roRegistrySection|roRemoteInfo|roResourceManager|roRssArticle|roRssParser|roRtspStream|roRtspStreamEvent|roSGNode|roSGNodeEvent|roSGScreen|roSGScreenEvent|roScreen|roSearchHistory|roSearchScreen|roSearchScreenEvent|roSequenceMatchEvent|roSequenceMatcher|roSerialPort|roSlideShow|roSlideShowEvent|roSnmpAgent|roSnmpEvent|roSocketAddress|roSocketEvent|roSpringboardScreen|roSpringboardScreenEvent|roSprite|roSqliteDatabase|roSqliteEvent|roSqliteStatement|roStorageAttached|roStorageDetached|roStorageHotplug|roStorageInfo|roStreamByteEvent|roStreamConnectResultEvent|roStreamEndEvent|roStreamLineEvent|roStreamQueue|roStreamQueueEvent|roStreamSocket|roString|roSyncManager|roSyncManagerEvent|roSyncPool|roSyncPoolEvent|roSyncPoolFiles|roSyncPoolProgressEvent|roSyncSpec|roSystemLog|roSystemLogEvent|roSystemTime|roTCPConnectEvent|roTCPServer|roTCPStream|roTextField|roTextScreen|roTextScreenEvent|roTextToSpeech|roTextToSpeechEvent|roTextWidget|roTextWidgetEvent|roTextureManager|roTextureRequest|roTextureRequestEvent|roTimeSpan|roTimer|roTimerEvent|roTimespan|roTouchCalibrationEvent|roTouchEvent|roTouchScreen|roTuner|roTunerEvent|roUPnPActionResult|roUPnPController|roUPnPDevice|roUPnPSearchEvent|roUPnPService|roUPnPServiceEvent|roUniversalControlEvent|roUrlEvent|roUrlTransfer|roUsbFilesystem|roUsbHidEmulator|roUsbHidLedEmulatorEvent|roUsbPowerControl|roVideoEvent|roVideoInput|roVideoMode|roVideoPlayer|roVideoPlayerEvent|roVideoScreen|roVideoScreenEvent|roVirtualMemory|roWriteFile|roXMLElement|roXMLList)(?=\"))", + "name": "support.class.brs" + }, + "region_comment": { + "match": "(?i:^\\s*('\\s*#region)(?:(\\s+.*)?))$", + "captures": { + "1": { + "name": "keyword.preprocessor.region.brs" + }, + "2": { + "name": "string.unquoted.preprocessor.message.brs" + } + } + }, + "end_region_comment": { + "match": "^(i?\\s*'\\s*#endregion(\\s*.*)?$)", + "name": "keyword.preprocessor.region.brs" + }, + "vscode_rale_tracker_entry_comment": { + "match": "('\\s*vscode_rale_tracker_entry[^\\S\\r\\n]*)", + "name": "keyword.preprocessor.brs" + }, + "bsdoc": { + "patterns": [ + { + "include": "#bsdoc_param_and_type" + }, + { + "include": "#bsdoc_return_and_type" + }, + { + "include": "#bsdoc_generic" + } + ] + }, + "bsdoc_generic": { + "match": "(?i:\\s*(rem|'+)\\s*(@)(\\w+)(.*$))", + "captures": { + "1": { + "name": "comment.line.apostrophe.brs" + }, + "2": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "3": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "4": { + "name": "comment.block.documentation.brs" + } + } + }, + "bsdoc_param_and_type": { + "match": "(?i:\\s*(rem|'+)\\s*(@)(param|type)\\s+(?:(\\{)\\s*([^\\}]+)\\s*(\\}))?\\s*([a-z0-9_.]+)?(.*))", + "captures": { + "1": { + "name": "comment.line.apostrophe.brs" + }, + "2": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "3": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "4": { + "name": "punctuation.definition.bracket.curly.begin.bsdoc" + }, + "5": { + "name": " comment.block.documentation.brs entity.name.type.instance.bsdoc" + }, + "6": { + "name": "punctuation.definition.bracket.curly.end.bsdoc" + }, + "7": { + "name": "variable.other.bsdoc" + }, + "8": { + "name": "comment.block.documentation.brs" + } + } + }, + "bsdoc_return_and_type": { + "match": "(?i:\\s*(rem|'+)\\s*(@)(return|returns)\\s+(?:(\\{)\\s*([^\\}]+)\\s*(\\}))?\\s*([a-z0-9_.]+.*)?)", + "captures": { + "1": { + "name": "comment.line.apostrophe.brs" + }, + "2": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "3": { + "name": "punctuation.definition.block.tag.bsdoc storage.type.class.bsdoc" + }, + "4": { + "name": "punctuation.definition.bracket.curly.begin.bsdoc" + }, + "5": { + "name": " comment.block.documentation.brs entity.name.type.instance.bsdoc" + }, + "6": { + "name": "punctuation.definition.bracket.curly.end.bsdoc" + }, + "7": { + "name": "comment.block.documentation.brs" + } + } + }, + "annotation": { + "patterns": [ + { + "match": "^\\s*(@)\\s*([a-zA-Z0-9_]+)", + "captures": { + "1": { + "name": "punctuation.decorator.brs" + }, + "2": { + "name": "entity.name.function.brs meta.function-call.brs" + } + } + }, + { + "name": "meta.decorator.brs", + "begin": "(@)\\s*([a-zA-Z0-9_]+)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.decorator.brs" + }, + "2": { + "name": "entity.name.function.brs meta.function-call.brs" + }, + "3": { + "name": "meta.brace.round.brs" + } + }, + "patterns": [ + { + "include": "#entire_language" + } + ], + "end": "(\\))", + "endCaptures": { + "1": { + "name": "meta.brace.round.brs" + } + } + } + ] + } + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..517fff4 --- /dev/null +++ b/renovate.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>jellyrock/.github//renovate/default"], + "customManagers": [ + { + "customType": "regex", + "description": "Track the rokucommunity/vscode-brightscript-language release whose TextMate grammar we vendor in dev-docs/src/syntaxes/. When Renovate bumps the ref, the sync-brightscript-grammar workflow re-fetches the JSON so the PR contains both changes atomically.", + "fileMatch": ["^dev-docs/src/syntaxes/\\.version-info$"], + "matchStrings": [ + "# renovate:\\s+datasource=(?\\S+)\\s+depName=(?\\S+)\\s*\\n\\s*ref:\\s*(?\\S+)" + ] + } + ], + "packageRules": [ + { + "description": "Label vendored-grammar bumps so they're easy to spot in the queue; do not automerge — humans review the upstream release notes before adopting.", + "matchPackageNames": ["rokucommunity/vscode-brightscript-language"], + "addLabels": ["vendored-grammar"], + "automerge": false + } + ] +} diff --git a/shared/fetch-docs.mjs b/shared/fetch-docs.mjs index a21a158..07e3027 100644 --- a/shared/fetch-docs.mjs +++ b/shared/fetch-docs.mjs @@ -197,8 +197,17 @@ starlight-toc nav a[aria-current='true'] { } /** - * Recursively copy markdown files, auto-generating Starlight frontmatter - * (title) from the first H1 heading when frontmatter is missing. + * Recursively copy markdown files, ensuring each one has a Starlight-required + * `title:` frontmatter. Three cases: + * 1. No frontmatter at all → generate one from H1 / HTML h1 / filename + * 2. Frontmatter without title: (e.g. jellyrock's docs use `topic:` only) + * → inject a title line into the existing block; rest of the + * frontmatter is preserved (topic:, last-reviewed:, etc.) + * 3. Frontmatter with title: → pass through unchanged + * + * The H1 is removed when used as the title source, so Starlight doesn't + * render it twice. + * * Non-markdown files are copied as-is. */ function copyMarkdownWithFrontmatter(srcDir, destDir) { @@ -219,17 +228,34 @@ function copyMarkdownWithFrontmatter(srcDir, destDir) { let content = fs.readFileSync(srcPath, 'utf8'); - // Skip if frontmatter already exists - if (content.startsWith('---\n')) { - fs.writeFileSync(destPath, content); - continue; + // Case 1+2 share title-extraction logic; just differ in what we + // write at the end. + const hasFrontmatter = content.startsWith('---\n'); + let existingFm = ''; + let body = content; + if (hasFrontmatter) { + const fmEnd = content.indexOf('\n---\n', 4); + if (fmEnd === -1) { + // Malformed (opens with --- but never closes). Pass through and + // let astro check report the real issue. + fs.writeFileSync(destPath, content); + continue; + } + existingFm = content.slice(4, fmEnd); + body = content.slice(fmEnd + 5); + + // Case 3 — already has title: + if (/^title:/m.test(existingFm)) { + fs.writeFileSync(destPath, content); + continue; + } } // Extract title from markdown H1, HTML

, or filename as fallback let title; let matchedLine; - const mdH1 = content.match(/^#\s+(.+?)\s*$/m); - const htmlH1 = content.match(/]*>\s*(.+?)\s*<\/h1>/i); + const mdH1 = body.match(/^#\s+(.+?)\s*$/m); + const htmlH1 = body.match(/]*>\s*(.+?)\s*<\/h1>/i); if (mdH1) { title = mdH1[1].trim(); @@ -247,10 +273,11 @@ function copyMarkdownWithFrontmatter(srcDir, destDir) { // Remove the matched H1 since Starlight renders title separately if (matchedLine) { - content = content.replace(matchedLine, '').replace(/^\s+/, ''); + body = body.replace(matchedLine, '').replace(/^\s+/, ''); } - const frontmatter = `---\ntitle: ${JSON.stringify(title)}\n---\n\n`; - fs.writeFileSync(destPath, frontmatter + content); + const titleLine = `title: ${JSON.stringify(title)}`; + const newFm = existingFm ? `---\n${titleLine}\n${existingFm}\n---\n\n` : `---\n${titleLine}\n---\n\n`; + fs.writeFileSync(destPath, newFm + body); } }