Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions grammar.ne
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ JVALUE -> JOBJECT {% (d) => d[0] %}
| "'" _ JOBJECT _ "'" {% (d) => d[2] %}
| JARRAY {% (d) => d[0] %}
| STRING {% (d) => d[0] %}
| SINGLE_QUOTED_STRING {% (d) => d[0] %}
| "null" {% (d) => null %}

JOBJECT -> "{" _ "}" {% (d) => { return { type: 'compound', value: {} } } %}
Expand All @@ -21,6 +22,8 @@ PAIR -> STRING _ ":" _ JVALUE {% (d) => [d[0].value, d[4]] %}
STRING -> "\"" ( [^\\"] | "\\" ["bfnrt\/\\] | "\\u" [a-fA-F0-9] [a-fA-F0-9] [a-fA-F0-9] [a-fA-F0-9] ):* "\"" {% (d) => parseValue( JSON.parse(d.flat(3).map(b => b.replace('\n', '\\n')).join('')) ) %}
| [^\"\'}\]:;,\s]:+ {% (d) => parseValue(d[0].join('')) %}

SINGLE_QUOTED_STRING -> "'" ( [^\\'] | "\\" ["bfnrt\/\\'] | "\\u" [a-fA-F0-9] [a-fA-F0-9] [a-fA-F0-9] [a-fA-F0-9] ):* "'" {% (d) => parseSingleQuoteString(d) %}

@{%

// Because of unquoted strings, parsing can be ambiguous.
Expand Down Expand Up @@ -48,6 +51,38 @@ function parseValue (str) {
return { value: str, type: 'string' }
}

function parseSingleQuoteString(d) {
// Build the string content from the parsed parts similar to double-quoted strings
// The structure is: ["'", [content parts], "'"]
// d[1] contains the content between quotes
const content = d[1] || []
let str = "'"
for (const part of content) {
if (Array.isArray(part)) {
str += part.flat().join('')
} else if (part) {
str += part
}
}
str += "'"

// Process escape sequences to convert to actual string value
// Replace escaped single quotes with actual single quotes
// and handle other escape sequences
str = str.replace(/\\'/g, "\\'") // Keep escaped single quotes for JSON parsing
.replace(/\\"/g, '\\"') // Keep escaped double quotes

// Convert to a JSON-compatible string by replacing outer single quotes with double quotes
str = '"' + str.slice(1, -1).replace(/"/g, '\\"').replace(/\\'/g, "'") + '"'

try {
return { value: JSON.parse(str), type: 'string' }
} catch (e) {
// If JSON parsing fails, return the raw string content
return { value: str.slice(1, -1), type: 'string' }
}
}

function extractPair(kv, output) {
if (kv[0] !== undefined) {
output[kv[0]] = kv[1]
Expand Down
57 changes: 56 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,68 @@ function parse (text) {
try {
const parserNE = new nearley.Parser(nearley.Grammar.fromCompiled(grammar))
parserNE.feed(text)
return parserNE.results[0]
// When there are multiple parse results (ambiguous grammar),
// prefer results with more structured types (compound, list) over strings
const results = parserNE.results
if (results.length > 1) {
// Score each result based on how "structured" it is
const scored = results.map((r, i) => {
const score = scoreResult(r)
return { result: r, score }
})
// Sort by score descending (higher score = more structured)
scored.sort((a, b) => b.score - a.score)
return scored[0].result
}
return results[0]
} catch (e) {
e.message = `Error parsing text '${text}'`
throw e
}
}

function scoreResult (obj) {
let score = 0
if (!obj || typeof obj !== 'object') return score

// Prefer compound and list types over string types
if (obj.type === 'compound') score += 10
if (obj.type === 'list') score += 10
if (obj.type === 'string') score -= 1

// Recursively score nested structures
if (obj.value && typeof obj.value === 'object') {
if (Array.isArray(obj.value)) {
obj.value.forEach(item => {
score += scoreResult(item)
})
} else if (obj.value.value && Array.isArray(obj.value.value)) {
// This is a list type with value.value array
// The items in this array are raw values, not wrapped in { type, value }
// Score based on whether items are objects (compounds) vs primitives
obj.value.value.forEach(item => {
if (item && typeof item === 'object' && !Array.isArray(item)) {
// This is likely a compound object
score += 10
// Recursively score the object's properties
Object.values(item).forEach(prop => {
score += scoreResult(prop)
})
} else if (typeof item === 'string') {
score -= 1
}
})
} else {
// This is a compound with nested values
Object.values(obj.value).forEach(item => {
score += scoreResult(item)
})
}
}

return score
}

module.exports = {
parse,
simplify,
Expand Down
10 changes: 9 additions & 1 deletion test/test.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.