Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
128 changes: 128 additions & 0 deletions csharp/Link.Foundation.Links.Notation.Tests/MultiRefTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using Xunit;

namespace Link.Foundation.Links.Notation.Tests
{
/// <summary>
/// Multi-Reference Feature Tests (Issue #184)
/// Tests for multi-word references without quotes:
/// - (some example: some example is a link)
/// - ID as multi-word string: "some example"
/// </summary>
public static class MultiRefTests
{
[Fact]
public static void ParsesTwoWordMultiReferenceId()
{
var parser = new Parser();
var result = parser.Parse("(some example: value)");
Assert.Single(result);
// Multi-word ID should be joined with space
Assert.Equal("some example", result[0].Id);
Assert.Single(result[0].Values);
}

[Fact]
public static void ParsesThreeWordMultiReferenceId()
{
var parser = new Parser();
var result = parser.Parse("(new york city: value)");
Assert.Single(result);
Assert.Equal("new york city", result[0].Id);
}

[Fact]
public static void ParsesSingleWordIdBackwardCompatible()
{
var parser = new Parser();
var result = parser.Parse("(papa: value)");
Assert.Single(result);
Assert.Equal("papa", result[0].Id);
}

[Fact]
public static void ParsesQuotedMultiWordIdBackwardCompatible()
{
var parser = new Parser();
var result = parser.Parse("('some example': value)");
Assert.Single(result);
// Quoted ID should be preserved as-is
Assert.Equal("some example", result[0].Id);
}

[Fact]
public static void FormatMultiReferenceId()
{
var parser = new Parser();
var result = parser.Parse("(some example: value)");
var formatted = result.Format();
// Multi-reference IDs are formatted with quotes (normalized form)
Assert.Equal("('some example': value)", formatted);
}

[Fact]
public static void RoundTripMultiReference()
{
var parser = new Parser();
var input = "(new york city: great)";
var result = parser.Parse(input);
var formatted = result.Format();
// Round-trip normalizes multi-word ID to quoted form
Assert.Equal("('new york city': great)", formatted);
}

[Fact]
public static void ParsesIndentedSyntaxMultiReference()
{
var parser = new Parser();
var input = "some example:\n value1\n value2";
var result = parser.Parse(input);
Assert.Single(result);
Assert.Equal("some example", result[0].Id);
Assert.Equal(2, result[0].Values?.Count);
}

[Fact]
public static void BackwardCompatibilitySingleLine()
{
var parser = new Parser();
var result = parser.Parse("papa: loves mama");
Assert.Single(result);
Assert.Equal("papa", result[0].Id);
Assert.Equal(2, result[0].Values?.Count);
}

[Fact]
public static void BackwardCompatibilityParenthesized()
{
var parser = new Parser();
var result = parser.Parse("(papa: loves mama)");
Assert.Single(result);
Assert.Equal("papa", result[0].Id);
Assert.Equal(2, result[0].Values?.Count);
}

[Fact]
public static void BackwardCompatibilityNested()
{
var parser = new Parser();
var result = parser.Parse("(outer: (inner: value))");
Assert.Single(result);
Assert.Equal("outer", result[0].Id);
Assert.Single(result[0].Values);
Assert.Equal("inner", result[0].Values?[0].Id);
}

[Fact]
public static void MultiRefWithMultipleValues()
{
var parser = new Parser();
var result = parser.Parse("(some example: one two three)");
Assert.Single(result);
Assert.Equal("some example", result[0].Id);
Assert.Equal(3, result[0].Values?.Count);
Assert.Equal("one", result[0].Values?[0].Id);
Assert.Equal("two", result[0].Values?[1].Id);
Assert.Equal("three", result[0].Values?[2].Id);
}
}
}
11 changes: 8 additions & 3 deletions csharp/Link.Foundation.Links.Notation/Parser.peg
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,16 @@ multiLineValueAndWhitespace <Link<string>> = value:referenceOrLink _ { value }
multiLineValues <IList<Link<string>>> = _ list:multiLineValueAndWhitespace* { list }
singleLineValueAndWhitespace <Link<string>> = __ value:referenceOrLink { value }
singleLineValues <IList<Link<string>>> = list:singleLineValueAndWhitespace+ { list }
singleLineLink <Link<string>> = __ id:(reference) __ ":" v:singleLineValues { new Link<string>(id, v) }
multiLineLink <Link<string>> = "(" _ id:(reference) _ ":" v:multiLineValues _ ")" { new Link<string>(id, v) }
singleLineLink <Link<string>> = __ id:multiRefId __ ":" v:singleLineValues { new Link<string>(id, v) }
multiLineLink <Link<string>> = "(" _ id:multiRefId _ ":" v:multiLineValues _ ")" { new Link<string>(id, v) }
singleLineValueLink <Link<string>> = v:singleLineValues { new Link<string>(v) }
multiLineValueLink <Link<string>> = "(" v:multiLineValues _ ")" { new Link<string>(v) }
indentedIdLink <Link<string>> = id:(reference) __ ":" eol { new Link<string>(id) }
indentedIdLink <Link<string>> = id:multiRefId __ ":" eol { new Link<string>(id) }

// Multi-reference ID: space-separated words before colon (joined with space)
// For backward compatibility, single word remains as-is
multiRefId <string> = refs:multiRefIdParts { string.Join(" ", refs) }
multiRefIdParts <IList<string>> = first:reference rest:(__ !(":" / eol / ")") r:reference { r })* { new List<string> { first }.Concat(rest).ToList() }

// Reference can be quoted (with any number of quotes) or simple unquoted
// Order: high quotes (3+) first, then double quotes (2), then single quotes (1), then simple
Expand Down
61 changes: 61 additions & 0 deletions experiments/multi_reference_design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Multi-Reference Feature Design (Issue #184)

## Overview

This document outlines the design for supporting multi-references in Links Notation.

## Current Behavior

```
Input: (papa: loves mama)
Parsed: Link(id="papa", values=[Ref("loves"), Ref("mama")])
```

For multi-word references, quoting is required:
```
Input: ('some example': value)
Parsed: Link(id="some example", values=[Ref("value")])
```

## Proposed Behavior

### Multi-Reference Definition

When a colon appears after multiple space-separated words, those words form a multi-reference:

```
Input: (some example: some example is a link)
Parsed: Link(id=["some", "example"], values=[MultiRef(["some", "example"]), Ref("is"), Ref("a"), Ref("link")])
```

### Key Changes

1. **ID field becomes an array**:
- Single-word: `id = ["papa"]`
- Multi-word: `id = ["some", "example"]`

2. **Values remain an array** but can contain multi-references:
- `values = [MultiRef(["some", "example"]), Ref("is"), ...]`

3. **Context-aware parsing**:
- First pass: Identify all multi-reference definitions (IDs before colons)
- Second pass: When parsing values, check if consecutive tokens form a known multi-reference

## Implementation Strategy

### Phase 1: Data Structure Changes
- Change `id` from `string | null` to `string[] | null`
- Add helper methods for multi-reference comparison

### Phase 2: Parser Changes
- Collect multi-reference definitions during parsing
- When parsing values, check for multi-reference matches

### Phase 3: Formatter Changes
- Format multi-word IDs without quotes (when possible)
- Preserve backward compatibility with quoted strings

## Backward Compatibility

- Quoted strings (`'some example'`) still work as single-token references
- Single-word IDs work the same way: `papa` -> `id = ["papa"]`
119 changes: 119 additions & 0 deletions experiments/test_multi_reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Multi-Reference Feature Experiment (Issue #184)
*
* This script tests the concept of multi-references where
* multiple space-separated words before a colon form a single reference.
*/

import { Parser, Link, formatLinks } from '../js/src/index.js';

const parser = new Parser();

console.log('=== Multi-Reference Feature Tests (Issue #184) ===\n');

// Test 1: Single-word ID (backward compatibility)
const test1 = 'papa: loves mama';
console.log('Test 1 - Single-word ID (backward compatible):');
console.log('Input:', test1);
try {
const result1 = parser.parse(test1);
console.log('Parsed:', JSON.stringify(result1, null, 2));
console.log('Formatted:', formatLinks(result1, true));
console.log('✅ Pass: Single-word ID still works');
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

// Test 2: Quoted multi-word ID (backward compatibility)
const test2 = "('some example': value)";
console.log('Test 2 - Quoted multi-word ID (backward compatible):');
console.log('Input:', test2);
try {
const result2 = parser.parse(test2);
console.log('Parsed:', JSON.stringify(result2, null, 2));
console.log('Formatted:', formatLinks(result2, true));
console.log('✅ Pass: Quoted multi-word ID still works');
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

// Test 3: Unquoted multi-word ID (NEW FEATURE)
const test3 = '(some example: some example is a link)';
console.log('Test 3 - Unquoted multi-word ID (NEW):');
console.log('Input:', test3);
try {
const result3 = parser.parse(test3);
console.log('Parsed:', JSON.stringify(result3, null, 2));
console.log('Formatted:', formatLinks(result3, true));
// Check if ID is an array with 2 elements
if (Array.isArray(result3[0].id) && result3[0].id.length === 2) {
console.log('✅ Pass: Multi-reference ID parsed as array:', result3[0].id);
} else {
console.log('⚠️ ID is not an array:', result3[0].id);
}
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

// Test 4: Context-aware multi-reference recognition in values
const test4 = '(some example: some example is a link)';
console.log('Test 4 - Context-aware multi-reference in values:');
console.log('Input:', test4);
try {
const result4 = parser.parse(test4);
console.log('Values count:', result4[0].values.length);
console.log('First value:', result4[0].values[0]);
// Check if "some example" in values is recognized as a single multi-ref
if (Array.isArray(result4[0].values[0].id) &&
result4[0].values[0].id.length === 2 &&
result4[0].values[0].id[0] === 'some' &&
result4[0].values[0].id[1] === 'example') {
console.log('✅ Pass: "some example" recognized as multi-reference in values');
} else {
console.log('⚠️ Multi-reference not recognized:', result4[0].values[0].id);
}
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

// Test 5: Multiple multi-references in one document
const test5 = `(some example: some example is a link)
some example`;
console.log('Test 5 - Self-reference (multi-ref used standalone):');
console.log('Input:', test5);
try {
const result5 = parser.parse(test5);
console.log('Parsed links count:', result5.length);
console.log('Second link:', JSON.stringify(result5[1], null, 2));
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

// Test 6: Mixed references (single and multi)
const test6 = '(new york city: new york city is great)';
console.log('Test 6 - Three-word multi-reference:');
console.log('Input:', test6);
try {
const result6 = parser.parse(test6);
console.log('Parsed:', JSON.stringify(result6, null, 2));
console.log('ID:', result6[0].id);
console.log('Values count:', result6[0].values.length);
if (Array.isArray(result6[0].id) && result6[0].id.length === 3) {
console.log('✅ Pass: 3-word multi-reference parsed correctly');
}
} catch (e) {
console.log('❌ Fail:', e.message);
}
console.log();

console.log('=== Summary ===\n');
console.log('Multi-reference feature implemented:');
console.log('1. Grammar updated to allow multiple references before colon');
console.log('2. ID field can now be string (single) or string[] (multi)');
console.log('3. Context-aware recognition: defined multi-refs recognized in values');
console.log('4. Backward compatible: single-word and quoted IDs still work');
Loading
Loading