diff --git a/package-lock.json b/package-lock.json index 46291039a68..056c2002ba2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "clipboardy": "^4.0.0", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", + "commonmark": "^0.31.2", + "gray-matter": "^4.0.3", "joi": "^18.0.1", "lz-string": "^1.5.0", "n-readlines": "^1.0.1", @@ -40,6 +42,7 @@ "@j-ulrich/release-it-regex-bumper": "^5.3.0", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", + "@types/commonmark": "^0.27.10", "@types/n-readlines": "^1.0.6", "@types/n3": "^1.26.0", "@types/object-hash": "^3.0.6", @@ -2576,6 +2579,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/commonmark": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@types/commonmark/-/commonmark-0.27.10.tgz", + "integrity": "sha512-iEZobUnvlM+UX5fXWCmC4eQXwCs01Z8Xa1W0VjiWUF/XsNy4BHtskqJ9MyLZVMHbA0ezhyonCDqz3hMvsCm6Hg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", @@ -4132,6 +4142,41 @@ "node": ">=12.20.0" } }, + "node_modules/commonmark": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.31.2.tgz", + "integrity": "sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg==", + "license": "BSD-2-Clause", + "dependencies": { + "entities": "~3.0.1", + "mdurl": "~1.0.1", + "minimist": "~1.2.8" + }, + "bin": { + "commonmark": "bin/commonmark" + }, + "engines": { + "node": "*" + } + }, + "node_modules/commonmark/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/commonmark/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -5538,7 +5583,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -5675,6 +5719,18 @@ "dev": true, "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-content-type-parse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", @@ -6290,6 +6346,49 @@ "dev": true, "license": "MIT" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gray-matter/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6780,6 +6879,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7412,6 +7520,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7980,7 +8097,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9729,6 +9845,19 @@ "dev": true, "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/seedrandom": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", @@ -10312,6 +10441,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", diff --git a/package.json b/package.json index 565f35e4a1a..73f8e24d2b6 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,7 @@ "@j-ulrich/release-it-regex-bumper": "^5.3.0", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", + "@types/commonmark": "^0.27.10", "@types/n-readlines": "^1.0.6", "@types/n3": "^1.26.0", "@types/object-hash": "^3.0.6", @@ -212,6 +213,8 @@ "clipboardy": "^4.0.0", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", + "commonmark": "^0.31.2", + "gray-matter": "^4.0.3", "joi": "^18.0.1", "lz-string": "^1.5.0", "n-readlines": "^1.0.1", diff --git a/src/benchmark/slicer.ts b/src/benchmark/slicer.ts index d170b904502..1e060421b81 100644 --- a/src/benchmark/slicer.ts +++ b/src/benchmark/slicer.ts @@ -5,7 +5,6 @@ import type { IStoppableStopwatch } from './stopwatch'; import { Measurements } from './stopwatch'; -import fs from 'fs'; import seedrandom from 'seedrandom'; import { log, LogLevel } from '../util/log'; import type { MergeableRecord } from '../util/objects'; @@ -29,8 +28,6 @@ import type { NormalizedAst, ParentInformation } from '../r-bridge/lang-4.x/ast/ import type { SlicingCriteria } from '../slicing/criterion/parse'; import type { DEFAULT_SLICING_PIPELINE, TREE_SITTER_SLICING_PIPELINE } from '../core/steps/pipeline/default-pipelines'; import { createSlicePipeline } from '../core/steps/pipeline/default-pipelines'; - - import type { RParseRequestFromFile, RParseRequestFromText } from '../r-bridge/retriever'; import { retrieveNumberOfRTokensOfLastParse } from '../r-bridge/retriever'; import type { PipelineStepNames, PipelineStepOutputWithName } from '../core/steps/pipeline/pipeline'; @@ -69,6 +66,7 @@ import { IntervalTop } from '../abstract-interpretation/data-frame/domain'; import { inferDataFrameShapes } from '../abstract-interpretation/data-frame/shape-inference'; +import fs from 'fs'; /** * The logger to be used for benchmarking as a global object. diff --git a/src/cli/slicer-app.ts b/src/cli/slicer-app.ts index adf6acbbf1b..242f14cc078 100644 --- a/src/cli/slicer-app.ts +++ b/src/cli/slicer-app.ts @@ -14,6 +14,7 @@ import { stats2string } from '../benchmark/stats/print'; import { makeMagicCommentHandler } from '../reconstruct/auto-select/magic-comments'; import { doNotAutoSelect } from '../reconstruct/auto-select/auto-select-defaults'; import { getConfig, getEngineConfig } from '../config'; +import { requestFromFile } from '../util/formats/adapter'; export interface SlicerCliOptions { verbose: boolean @@ -50,7 +51,7 @@ async function getSlice() { await slicer.init( options['input-is-text'] ? { request: 'text', content: options.input.replaceAll('\\n', '\n') } - : { request: 'file', content: options.input }, + : requestFromFile(options.input), config, options['no-magic-comments'] ? doNotAutoSelect : makeMagicCommentHandler(doNotAutoSelect) ); @@ -99,7 +100,11 @@ async function getSlice() { console.log(JSON.stringify(output, jsonReplacer)); } else { if(doSlicing && options.diff) { - const originalCode = options['input-is-text'] ? options.input : fs.readFileSync(options.input).toString(); + let originalCode = options.input; + if(!options['input-is-text']) { + const request = requestFromFile(options.input); + originalCode = request.request === 'text' ? request.content : fs.readFileSync(request.content).toString(); + } console.log(sliceDiffAnsi((slice as SliceResult).result, normalize, new Set(mappedSlices.map(({ id }) => id)), originalCode)); } if(options.stats) { diff --git a/src/r-bridge/lang-4.x/tree-sitter/tree-sitter-executor.ts b/src/r-bridge/lang-4.x/tree-sitter/tree-sitter-executor.ts index 81cb76101bd..458bd1e4afe 100644 --- a/src/r-bridge/lang-4.x/tree-sitter/tree-sitter-executor.ts +++ b/src/r-bridge/lang-4.x/tree-sitter/tree-sitter-executor.ts @@ -1,10 +1,10 @@ import Parser from 'web-tree-sitter'; import type { RParseRequest } from '../../retriever'; -import fs from 'fs'; import type { SyncParser } from '../../parser'; import type { TreeSitterEngineConfig } from '../../../config'; import { log } from '../../../util/log'; +import fs from 'fs'; export const DEFAULT_TREE_SITTER_R_WASM_PATH = './node_modules/@eagleoutice/tree-sitter-r/tree-sitter-r.wasm'; export const DEFAULT_TREE_SITTER_WASM_PATH = './node_modules/web-tree-sitter/tree-sitter.wasm'; diff --git a/src/r-bridge/retriever.ts b/src/r-bridge/retriever.ts index e44637bd95b..18fb3da0bf3 100644 --- a/src/r-bridge/retriever.ts +++ b/src/r-bridge/retriever.ts @@ -11,19 +11,31 @@ import { deterministicCountingIdGenerator } from './lang-4.x/ast/model/processin import { RawRType } from './lang-4.x/ast/model/type'; import fs from 'fs'; import path from 'path'; +import type { SupportedFormats } from '../util/formats/adapter-format'; +import { requestFromFile } from '../util/formats/adapter'; export const fileProtocol = 'file://'; -export interface RParseRequestFromFile { +export interface PraseRequestAdditionalInfoBase { + type: SupportedFormats +} + +export interface RParseRequestFromFile { readonly request: 'file'; /** * The path to the file (an absolute path is probably best here). * See {@link RParseRequests} for multiple files. */ readonly content: string; + + /** + * Aditional info from different file formates like .Rmd + */ + readonly info?: AdditionalInfo; + } -export interface RParseRequestFromText { +export interface RParseRequestFromText { readonly request: 'text' /** * Source code to parse (not a file path). @@ -32,6 +44,11 @@ export interface RParseRequestFromText { * or concatenate their contents to pass them with this request. */ readonly content: string + + /** + * Aditional info from different file formates like .Rmd + */ + readonly info?: AdditionalInfo; } /** @@ -66,10 +83,15 @@ export function requestFromInput(input: `${typeof fileProtocol}${string}` | stri } const content = input as string; const file = content.startsWith(fileProtocol); - return { - request: file ? 'file' : 'text', - content: file ? content.slice(7) : content - }; + + if(file) { + return requestFromFile(content.slice(7)); + } else { + return { + request: 'text', + content: content + }; + } } @@ -135,6 +157,7 @@ export function retrieveParseDataFromRCode(request: RParseRequest, shell: RShell if(isEmptyRequest(request)) { return Promise.resolve(''); } + const suffix = request.request === 'file' ? ', encoding="utf-8"' : ''; /* call the function with the request */ const command =`flowr_get_ast(${request.request}=${JSON.stringify( diff --git a/src/util/formats/adapter-format.ts b/src/util/formats/adapter-format.ts new file mode 100644 index 00000000000..02f0ed18b00 --- /dev/null +++ b/src/util/formats/adapter-format.ts @@ -0,0 +1,9 @@ +import type { RParseRequest } from '../../r-bridge/retriever'; + +export interface FileAdapter { + convertRequest(request: RParseRequest): RParseRequest +} + +export type SupportedFormats = 'R' | 'Rmd'; + +export type SupportedDocumentTypes = '.r' | '.rmd'; diff --git a/src/util/formats/adapter.ts b/src/util/formats/adapter.ts new file mode 100644 index 00000000000..0d5fef66c04 --- /dev/null +++ b/src/util/formats/adapter.ts @@ -0,0 +1,46 @@ +import type { RParseRequest, RParseRequestFromFile } from '../../r-bridge/retriever'; +import type { FileAdapter, SupportedDocumentTypes, SupportedFormats } from './adapter-format'; +import { RAdapter } from './adapters/r-adapter'; +import path from 'path'; +import { RmdAdapter } from './adapters/rmd-adapter'; + +export const FileAdapters = { + 'R': RAdapter, + 'Rmd': RmdAdapter +} as const satisfies Record; + +export const DocumentTypeToFormat = { + '.r': 'R', + '.rmd': 'Rmd' +} as const satisfies Record; + +export type AdapterReturnTypes = ReturnType; + +export function requestFromFile(path: string): AdapterReturnTypes { + const baseRequest = { + request: 'file', + content: path + } satisfies RParseRequestFromFile; + + + const type = inferFileType(baseRequest); + return FileAdapters[type].convertRequest(baseRequest); +} + +export function inferFileType(request: RParseRequest): keyof typeof FileAdapters { + if(request.request === 'text') { + // For now we don't know what type the request is + // and have to assume it is normal R Code + // In the future we could add a heuristic to guess the type + return 'R'; + } + + const type = path.extname(request.content).toLowerCase(); + + // Fallback to default if unknown + if(!Object.hasOwn(DocumentTypeToFormat, type)) { + return 'R'; + } + + return DocumentTypeToFormat[type as keyof typeof DocumentTypeToFormat]; +} diff --git a/src/util/formats/adapters/r-adapter.ts b/src/util/formats/adapters/r-adapter.ts new file mode 100644 index 00000000000..0af0b1569ae --- /dev/null +++ b/src/util/formats/adapters/r-adapter.ts @@ -0,0 +1,6 @@ +import type { RParseRequest } from '../../../r-bridge/retriever'; +import type { FileAdapter } from '../adapter-format'; + +export const RAdapter = { + convertRequest: (request: RParseRequest) => request +} satisfies FileAdapter; diff --git a/src/util/formats/adapters/rmd-adapter.ts b/src/util/formats/adapters/rmd-adapter.ts new file mode 100644 index 00000000000..a826eafeb4e --- /dev/null +++ b/src/util/formats/adapters/rmd-adapter.ts @@ -0,0 +1,119 @@ +import fs from 'fs'; +import type { Node } from 'commonmark'; +import { Parser } from 'commonmark'; +import matter from 'gray-matter'; +import { guard } from '../../assert'; +import type { FileAdapter } from '../adapter-format'; +import type { RParseRequest, RParseRequestFromText } from '../../../r-bridge/retriever'; + +export interface CodeBlock { + options: string, + code: string, +} + +export type CodeBlockEx = CodeBlock & { + startpos: { line: number, col: number } +} + +export interface RmdInfo { + type: 'Rmd' + blocks: CodeBlock[] + options: object +} + +export const RmdAdapter = { + convertRequest: (request: RParseRequest) => { + // Read and Parse Markdown + const raw = request.request === 'text' + ? request.content + : fs.readFileSync(request.content, 'utf-8').toString(); + + const parser = new Parser(); + const ast = parser.parse(raw); + + // Parse Frontmatter + const frontmatter = matter(raw); + + // Parse Codeblocks + const walker = ast.walker(); + const blocks: CodeBlockEx[] = []; + let e; + while((e = walker.next())) { + const node = e.node; + if(!isRCodeBlock(node)) { + continue; + } + + blocks.push({ + code: node.literal, + options: parseCodeBlockOptions(node.info, node.literal), + startpos: { line: node.sourcepos[0][0] + 1, col: 0 } + }); + } + + return { + request: 'text', + content: restoreBlocksWithoutMd(blocks, countNewlines(raw)), + info: { + // eslint-disable-next-line unused-imports/no-unused-vars + blocks: blocks.map(({ startpos, ...block }) => block), + options: frontmatter.data, + type: 'Rmd' + } + } as RParseRequestFromText; + + } +} satisfies FileAdapter; + + +const RTagRegex = /{[rR](?:[\s,][^}]*)?}/; +export function isRCodeBlock(node: Node): node is Node & { literal: string, info: string } { + return node.type === 'code_block' && node.literal !== null && node.info !== null && RTagRegex.test(node.info); +} + +const LineRegex = /\r\n|\r|\n/; +function countNewlines(str: string): number { + return str.split(LineRegex).length - 1; +} + +export function restoreBlocksWithoutMd(blocks: CodeBlockEx[], totalLines: number): string { + let line = 1; + let output = ''; + + const goToLine = (n: number) => { + const diff = n - line; + guard(diff >= 0); + line += diff; + output += '\n'.repeat(diff); + }; + + for(const block of blocks) { + goToLine(block.startpos.line); + output += block.code; + line += countNewlines(block.code); + } + + // Add remainder of file + goToLine(totalLines + 1); + + return output; +} + +export function parseCodeBlockOptions(header: string, content: string): string { + let opts = header.length === 3 // '{r}' => header.length=3 (no options in header) + ? '' + : header.substring(3, header.length-1).trim(); + + const lines = content.split('\n'); + for(const line of lines) { + if(!line.trim().startsWith('#|')) { + break; + } + + const opt = line.substring(3); + + opts += opts.length === 0 ? opt : `, ${opt}`; + } + + return opts; +} \ No newline at end of file diff --git a/test/functionality/util/formats/adapter.test.ts b/test/functionality/util/formats/adapter.test.ts new file mode 100644 index 00000000000..08a35ea3f47 --- /dev/null +++ b/test/functionality/util/formats/adapter.test.ts @@ -0,0 +1,17 @@ +import { assert, describe, test } from 'vitest'; +import { inferFileType } from '../../../../src/util/formats/adapter'; + +describe('format adapter', () => { + test.each([ + ['.R', 'R'], + ['.r', 'R'], + ['.rmd', 'Rmd'], + ['.Rmd', 'Rmd'], + ['.whoknows', 'R'], + ])('inferFileType(\'%s\') -> %s', (ext, expected) => { + assert.equal(inferFileType({ + request: 'file', + content: `/some/file${ext}` + }), expected); + }); +}); \ No newline at end of file diff --git a/test/functionality/util/formats/rmd-format.test.ts b/test/functionality/util/formats/rmd-format.test.ts new file mode 100644 index 00000000000..f7d06bc3027 --- /dev/null +++ b/test/functionality/util/formats/rmd-format.test.ts @@ -0,0 +1,143 @@ +import { assert, describe, test } from 'vitest'; +import { requestFromFile } from '../../../../src/util/formats/adapter'; +import { restoreBlocksWithoutMd, isRCodeBlock, type RmdInfo } from '../../../../src/util/formats/adapters/rmd-adapter'; +import { Node } from 'commonmark'; +import type { RParseRequestFromText } from '../../../../src/r-bridge/retriever'; + +describe('rmd', () => { + describe('utility functions', () => { + test.each([ + /* Positive Cases */ + ['{r}', true], + ['{R}', true], + ['{r, some.options=5}', true], + ['{r, name, option=3}', true], + ['{r some.options=5}', true], + ['{R name, option=3}', true], + /* Negative Cases */ + ['{rust}', false], + ['{c}', false], + ['r', false], + ])('isRCodeBlock(\'%s\') -> %s', (str, expected) => { + const node = new Node('code_block'); + node.literal = 'Test'; + node.info = str; + assert.equal(isRCodeBlock(node), expected); + }); + + + test.each([ + [ // #1 - simple + [ + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 1, + col: 1 + } + }, + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 2, + col: 1 + } + } + ], + 2, + 'Hello World\nHello World\n' + ], + [ // #2 - new lines at end + [ + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 1, + col: 1 + } + }, + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 2, + col: 1 + } + } + ], + 4, + 'Hello World\nHello World\n\n\n' + ], + [ // #3 - new lines between and at end + [ + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 1, + col: 1 + } + }, + { + options: 'dont care', + code: 'Hello World\n', + startpos: { + line: 5, + col: 1 + } + } + ], + 7, + 'Hello World\n\n\n\nHello World\n\n\n' + ] + ])('resotre block (%#)', (blocks, lines, expected) => { + const restored = restoreBlocksWithoutMd(blocks, lines); + assert.equal(restored, expected); + }); + }); + + + test('load simple', () => { + const data = requestFromFile('test/testfiles/notebook/example.Rmd'); + assert.deepEqual(data, { + request: 'text', + content: '\n\n\n\n\n\n\n\n\n\n' + + 'test <- 42\n' + + 'cat(test)\n\n\n\n\n' + + 'x <- "Hello World"\n\n\n\n\n' + + ' cat("Hi")\n\n\n\n\n\n' + + '#| cache=FALSE\n' + + 'cat(test)\n\n\n\n\n\n\n\n\n\n' + + 'v <- c(1,2,3)\n\n\n\n', + info: { + type: 'Rmd', + blocks: [ + { + code: 'test <- 42\ncat(test)\n', + options: '', + }, + { + code: 'x <- "Hello World"\n', + options: 'abc', + }, + { + code: ' cat("Hi")\n', + options: 'ops, echo=FALSE', + }, + { + code: '#| cache=FALSE\ncat(test)\n', + options: 'echo=FALSE, cache=FALSE', + }, + { + code: 'v <- c(1,2,3)\n', + options: 'test' + } + ], + options: { title: 'Sample Document', output: 'pdf_document' } + } + } satisfies RParseRequestFromText); + }); +}); diff --git a/test/testfiles/notebook/example.Rmd b/test/testfiles/notebook/example.Rmd new file mode 100644 index 00000000000..d333dedb72e --- /dev/null +++ b/test/testfiles/notebook/example.Rmd @@ -0,0 +1,42 @@ +--- +title: "Sample Document" +output: pdf_document +--- + +# Hello World +This is a test + +Simple Code R Chunk: +```{r} +test <- 42 +cat(test) +``` + +With Label `r test` +```{r abc} +x <- "Hello World" +``` + +With Options +```{r ops, echo=FALSE} + cat("Hi") +``` + + +With Body Opts +```{r, echo=FALSE} +#| cache=FALSE +cat(test) +``` + + +Unrelated Block `right here with unrelated code` +```c +printf("Hello World"); +``` + +```{r, test} +v <- c(1,2,3) +``` + +