diff --git a/main.rb b/main.rb new file mode 100644 index 000000000..5b574e780 --- /dev/null +++ b/main.rb @@ -0,0 +1,6 @@ +require 'json' + +file_path = '/Users/rileynowak/Downloads/theme_export__stage-13-jhfdhs-uk-myshopify-com-theme-export-shop-idlesband-com-theme-export-s__22NOV2024-0413pm/config/settings_schema.json' +json_content = File.read(file_path) +parsed_json = JSON.parse(json_content) +File.write(file_path, parsed_json.to_json) diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts index 42b82c2dc..749c4e398 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.spec.ts @@ -61,4 +61,20 @@ describe('DocumentLinksProvider', () => { expect(result[i].target).toBe(expectedUrls[i]); } }); + + it('should return a list of document links with correct URLs for a LiquidRawTag document', async () => { + uriString = 'file:///path/to/liquid-raw-tag-document.liquid'; + rootUri = 'file:///path/to/project'; + + const liquidRawTagContent = ` + {% schema %} + { "blocks": [{ "type": "valid" }] } + {% endschema %} + `; + + documentManager.open(uriString, liquidRawTagContent, 1); + + const result = await documentLinksProvider.documentLinks(uriString); + expect(result).toEqual([]); + }); }); diff --git a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts index 3541370b3..6ce49260b 100644 --- a/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts +++ b/packages/theme-language-server-common/src/documentLinks/DocumentLinksProvider.ts @@ -1,4 +1,4 @@ -import { LiquidHtmlNode, LiquidString, NodeTypes } from '@shopify/liquid-html-parser'; +import { LiquidHtmlNode, LiquidRawTag, LiquidString, NodeTypes } from '@shopify/liquid-html-parser'; import { SourceCodeType } from '@shopify/theme-check-common'; import { DocumentLink, Range } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -7,6 +7,13 @@ import { URI, Utils } from 'vscode-uri'; import { DocumentManager } from '../documents'; import { visit, Visitor } from '@shopify/theme-check-common'; +import { + parseTree, + findNodeAtLocation, + ParseError, + Node as JSONNode, +} from 'jsonc-parser'; + export class DocumentLinksProvider { constructor( private documentManager: DocumentManager, @@ -79,6 +86,39 @@ function documentLinksVisitor( Utils.resolvePath(root, 'assets', expression.value).toString(), ); }, + + LiquidRawTag(node) { + if (node.name === 'schema') { + const errors: ParseError[] = []; + const jsonNode = parseTree(node.body.value, errors); + if (!jsonNode || errors.length > 0) { + return []; + } + + const links: DocumentLink[] = []; + + // Process top-level blocks + const blocksNode = findNodeAtLocation(jsonNode, ['blocks']); + if (blocksNode && blocksNode.type === 'array' && blocksNode.children) { + links.push(...createLinksFromBlocks(blocksNode, node, textDocument, root)); + } + + // Process presets + const presetsNode = findNodeAtLocation(jsonNode, ['presets']); + if (presetsNode && presetsNode.type === 'array' && presetsNode.children) { + presetsNode.children.forEach((presetNode) => { + // Process blocks within each preset + const presetBlocksNode = findNodeAtLocation(presetNode, ['blocks']); + if (presetBlocksNode) { + links.push(...processPresetBlocks(presetBlocksNode, node, textDocument, root)); + } + }); + } + + return links; + } + return []; + }, }; } @@ -91,3 +131,153 @@ function range(textDocument: TextDocument, node: { position: LiquidHtmlNode['pos function isLiquidString(node: LiquidHtmlNode): node is LiquidString { return node.type === NodeTypes.String; } + +function createDocumentLinkForTypeNode( + typeNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, + blockType: string, +): DocumentLink | null { + const startOffset = typeNode.offset; + const endOffset = typeNode.offset + typeNode.length; + const startPos = parentNode.body.position.start + startOffset; + const endPos = parentNode.body.position.start + endOffset; + + const start = textDocument.positionAt(startPos); + const end = textDocument.positionAt(endPos); + + return DocumentLink.create( + Range.create(start, end), + Utils.resolvePath(root, 'blocks', `${blockType}.liquid`).toString(), + ); +} + +function processPresetBlocks( + blocksNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, +): DocumentLink[] { + const links: DocumentLink[] = []; + + if (blocksNode.type === 'object' && blocksNode.children) { + blocksNode.children.forEach((propertyNode) => { + const blockValueNode = propertyNode.children?.[1]; // The value node of the property + if (!blockValueNode) return; + + // Check if the block has a 'name' key so we don't deeplink inline block types + const nameNode = findNodeAtLocation(blockValueNode, ['name']); + if (nameNode) { + return; + } + + const typeNode = findNodeAtLocation(blockValueNode, ['type']); + if (typeNode && typeNode.type === 'string' && typeof typeNode.value === 'string') { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + + // Recursively process nested blocks + const nestedBlocksNode = findNodeAtLocation(blockValueNode, ['blocks']); + if (nestedBlocksNode) { + links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); + } + }); + } else if (blocksNode.type === 'array' && blocksNode.children) { + blocksNode.children.forEach((blockNode) => { + // Check if the block has a 'name' key + const nameNode = findNodeAtLocation(blockNode, ['name']); + if (nameNode) { + return; // Skip creating a link if 'name' key exists + } + + const typeNode = findNodeAtLocation(blockNode, ['type']); + if (typeNode && typeNode.type === 'string' && typeof typeNode.value === 'string') { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + + // Recursively process nested blocks + const nestedBlocksNode = findNodeAtLocation(blockNode, ['blocks']); + if (nestedBlocksNode) { + links.push(...processPresetBlocks(nestedBlocksNode, parentNode, textDocument, root)); + } + }); + } + + return links; +} + +function createLinksFromBlocks( + blocksNode: JSONNode, + parentNode: LiquidRawTag, + textDocument: TextDocument, + root: URI, +): DocumentLink[] { + const links: DocumentLink[] = []; + + if (blocksNode.children) { + blocksNode.children.forEach((blockNode: JSONNode) => { + // Check if the block has a 'name' key to avoid deeplinking inline block types + const nameNode = findNodeAtLocation(blockNode, ['name']); + if (nameNode) { + return; + } + + const typeNode = findNodeAtLocation(blockNode, ['type']); + if ( + typeNode && + typeNode.type === 'string' && + typeof typeNode.value === 'string' + ) { + const blockType = typeNode.value; + if (blockType.startsWith('@')) { + return; + } + + const link = createDocumentLinkForTypeNode( + typeNode, + parentNode, + textDocument, + root, + blockType, + ); + + if (link) { + links.push(link); + } + } + }); + } + + return links; +} \ No newline at end of file