summaryrefslogtreecommitdiff
path: root/examples/server/public
diff options
context:
space:
mode:
Diffstat (limited to 'examples/server/public')
-rw-r--r--examples/server/public/completion.js4
-rw-r--r--examples/server/public/index-new.html12
-rw-r--r--examples/server/public/index.html182
-rw-r--r--examples/server/public/json-schema-to-grammar.mjs306
4 files changed, 463 insertions, 41 deletions
diff --git a/examples/server/public/completion.js b/examples/server/public/completion.js
index 987b9a3b..36818f76 100644
--- a/examples/server/public/completion.js
+++ b/examples/server/public/completion.js
@@ -21,7 +21,7 @@ let generation_settings = null;
//
export async function* llama(prompt, params = {}, config = {}) {
let controller = config.controller;
- const api_url = config.api_url || "";
+ const api_url = config.api_url?.replace(/\/+$/, '') || "";
if (!controller) {
controller = new AbortController();
@@ -196,7 +196,7 @@ export const llamaComplete = async (params, controller, callback) => {
// Get the model info from the server. This is useful for getting the context window and so on.
export const llamaModelInfo = async (config = {}) => {
if (!generation_settings) {
- const api_url = config.api_url || "";
+ const api_url = config.api_url?.replace(/\/+$/, '') || "";
const props = await fetch(`${api_url}/props`).then(r => r.json());
generation_settings = props.default_generation_settings;
}
diff --git a/examples/server/public/index-new.html b/examples/server/public/index-new.html
index 19c9f643..c87dd8f1 100644
--- a/examples/server/public/index-new.html
+++ b/examples/server/public/index-new.html
@@ -14,10 +14,10 @@
<script type="module">
import {
html, h, signal, effect, computed, render, useSignal, useEffect, useRef, Component
- } from '/index.js';
+ } from './index.js';
- import { llama } from '/completion.js';
- import { SchemaConverter } from '/json-schema-to-grammar.mjs';
+ import { llama } from './completion.js';
+ import { SchemaConverter } from './json-schema-to-grammar.mjs';
import { promptFormats } from './prompt-formats.js';
import { systemPrompts } from './system-prompts.js'; // multilingual is wip
let selected_image = false;
@@ -225,7 +225,7 @@
throw new Error("already running");
}
controller.value = new AbortController();
- for await (const chunk of llama(prompt, llamaParams, { controller: controller.value })) {
+ for await (const chunk of llama(prompt, llamaParams, { controller: controller.value, api_url: new URL('.', document.baseURI).href })) {
const data = chunk.data;
if (data.stop) {
while (
@@ -634,12 +634,12 @@ return html`
<div>
<div class="grammar">
<label for="template"></label>
- <textarea id="grammar" name="grammar" placeholder="Use GBNF or JSON-Scheme + Converter" value="${params.value.grammar}" rows=4 oninput=${updateParams}/>
+ <textarea id="grammar" name="grammar" placeholder="Use GBNF or JSON Schema + Converter" value="${params.value.grammar}" rows=4 oninput=${updateParams}/>
</div>
<div class="grammar-columns">
<div class="json-schema-controls">
<input type="text" name="prop-order" placeholder="Order: prop1,prop2,prop3" oninput=${updateGrammarJsonSchemaPropOrder} />
- <button type="button" class="button-grammar" onclick=${convertJSONSchemaGrammar}>Convert JSON-Scheme</button>
+ <button type="button" class="button-grammar" onclick=${convertJSONSchemaGrammar}>Convert JSON Schema</button>
</div>
</div>
</div>
diff --git a/examples/server/public/index.html b/examples/server/public/index.html
index 2f60a76e..07fec6a3 100644
--- a/examples/server/public/index.html
+++ b/examples/server/public/index.html
@@ -1,5 +1,4 @@
<html>
-
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
@@ -132,12 +131,20 @@
align-items: stretch;
}
- .right {
+ .message-controls {
display: flex;
- flex-direction: row;
- gap: 0.5em;
justify-content: flex-end;
}
+ .message-controls > div:nth-child(2) {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+ }
+ .message-controls > div:nth-child(2) > div {
+ display: flex;
+ margin-left: auto;
+ gap: 0.5em;
+ }
fieldset {
border: none;
@@ -276,6 +283,7 @@
import { llama } from './completion.js';
import { SchemaConverter } from './json-schema-to-grammar.mjs';
+
let selected_image = false;
var slot_id = -1;
@@ -447,6 +455,9 @@
/* END: Support for storing prompt templates and parameters in browsers LocalStorage */
+ const tts = window.speechSynthesis;
+ const ttsVoice = signal(null)
+
const llamaStats = signal(null)
const controller = signal(null)
@@ -479,7 +490,7 @@
throw new Error("already running");
}
controller.value = new AbortController();
- for await (const chunk of llama(prompt, llamaParams, { controller: controller.value, api_url: location.pathname.replace(/\/+$/, '') })) {
+ for await (const chunk of llama(prompt, llamaParams, { controller: controller.value, api_url: new URL('.', document.baseURI).href })) {
const data = chunk.data;
if (data.stop) {
@@ -596,8 +607,51 @@
});
}
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ const talkRecognition = SpeechRecognition ? new SpeechRecognition() : null;
function MessageInput() {
- const message = useSignal("")
+ const message = useSignal("");
+
+ const talkActive = useSignal(false);
+ const sendOnTalk = useSignal(false);
+ const talkStop = (e) => {
+ if (e) e.preventDefault();
+
+ talkActive.value = false;
+ talkRecognition?.stop();
+ }
+ const talk = (e) => {
+ e.preventDefault();
+
+ if (talkRecognition)
+ talkRecognition.start();
+ else
+ alert("Speech recognition is not supported by this browser.");
+ }
+ if(talkRecognition) {
+ talkRecognition.onstart = () => {
+ talkActive.value = true;
+ }
+ talkRecognition.onresult = (e) => {
+ if (event.results.length > 0) {
+ message.value = event.results[0][0].transcript;
+ if (sendOnTalk.value) {
+ submit(e);
+ }
+ }
+ }
+ talkRecognition.onspeechend = () => {
+ talkStop();
+ }
+ }
+
+ const ttsVoices = useSignal(tts?.getVoices() || []);
+ const ttsVoiceDefault = computed(() => ttsVoices.value.find(v => v.default));
+ if (tts) {
+ tts.onvoiceschanged = () => {
+ ttsVoices.value = tts.getVoices();
+ }
+ }
const submit = (e) => {
stop(e);
@@ -624,11 +678,45 @@
value="${message}"
/>
</div>
- <div class="right">
- <button type="submit" disabled=${generating.value}>Send</button>
- <button onclick=${uploadImage}>Upload Image</button>
- <button onclick=${stop} disabled=${!generating.value}>Stop</button>
- <button onclick=${reset}>Reset</button>
+ <div class="message-controls">
+ <div> </div>
+ <div>
+ <div>
+ <button type="submit" disabled=${generating.value || talkActive.value}>Send</button>
+ <button disabled=${generating.value || talkActive.value} onclick=${uploadImage}>Upload Image</button>
+ <button onclick=${stop} disabled=${!generating.value}>Stop</button>
+ <button onclick=${reset}>Reset</button>
+ </div>
+ <div>
+ <a href="#" style="cursor: help;" title="Help" onclick=${e => {
+ e.preventDefault();
+ alert(`STT supported by your browser: ${SpeechRecognition ? 'Yes' : 'No'}\n` +
+ `(TTS and speech recognition are not provided by llama.cpp)\n` +
+ `Note: STT requires HTTPS to work.`);
+ }}>[?]</a>
+ <button disabled=${generating.value} onclick=${talkActive.value ? talkStop : talk}>${talkActive.value ? "Stop Talking" : "Talk"}</button>
+ <div>
+ <input type="checkbox" id="send-on-talk" name="send-on-talk" checked="${sendOnTalk}" onchange=${(e) => sendOnTalk.value = e.target.checked} />
+ <label for="send-on-talk" style="line-height: initial;">Send after talking</label>
+ </div>
+ </div>
+ <div>
+ <a href="#" style="cursor: help;" title="Help" onclick=${e => {
+ e.preventDefault();
+ alert(`TTS supported by your browser: ${tts ? 'Yes' : 'No'}\n(TTS and speech recognition are not provided by llama.cpp)`);
+ }}>[?]</a>
+ <label for="tts-voices" style="line-height: initial;">Bot Voice:</label>
+ <select id="tts-voices" name="tts-voices" onchange=${(e) => ttsVoice.value = e.target.value} style="max-width: 100px;">
+ <option value="" selected="${!ttsVoice.value}">None</option>
+ ${[
+ ...(ttsVoiceDefault.value ? [ttsVoiceDefault.value] : []),
+ ...ttsVoices.value.filter(v => !v.default),
+ ].map(
+ v => html`<option value="${v.name}" selected="${ttsVoice.value === v.name}">${v.name} (${v.lang}) ${v.default ? '(default)' : ''}</option>`
+ )}
+ </select>
+ </div>
+ </div>
</div>
</form>
`
@@ -659,26 +747,86 @@
}
}, [messages])
+ const ttsChatLineActiveIx = useSignal(undefined);
+ const ttsChatLine = (e, ix, msg) => {
+ if (e) e.preventDefault();
+
+ if (!tts || !ttsVoice.value || !('SpeechSynthesisUtterance' in window)) return;
+
+ const ttsVoices = tts.getVoices();
+ const voice = ttsVoices.find(v => v.name === ttsVoice.value);
+ if (!voice) return;
+
+ if (ttsChatLineActiveIx.value !== undefined) {
+ tts.cancel();
+ if (ttsChatLineActiveIx.value === ix) {
+ ttsChatLineActiveIx.value = undefined;
+ return;
+ }
+ }
+
+ ttsChatLineActiveIx.value = ix;
+ let ttsUtter = new SpeechSynthesisUtterance(msg);
+ ttsUtter.voice = voice;
+ ttsUtter.onend = e => {
+ ttsChatLineActiveIx.value = undefined;
+ };
+ tts.speak(ttsUtter);
+ }
+
const isCompletionMode = session.value.type === 'completion'
+
+ // Try play the last bot message
+ const lastCharChatLinesIxs = useSignal([]);
+ const lastCharChatLinesIxsOld = useSignal([]);
+ useEffect(() => {
+ if (
+ !isCompletionMode
+ && lastCharChatLinesIxs.value.length !== lastCharChatLinesIxsOld.value.length
+ && !generating.value
+ ) {
+ const ix = lastCharChatLinesIxs.value[lastCharChatLinesIxs.value.length - 1];
+ if (ix !== undefined) {
+ const msg = messages[ix];
+ ttsChatLine(null, ix, Array.isArray(msg) ? msg[1].map(m => m.content).join('') : msg);
+ }
+
+ lastCharChatLinesIxsOld.value = structuredClone(lastCharChatLinesIxs.value);
+ }
+ }, [generating.value]);
+
const chatLine = ([user, data], index) => {
let message
- const isArrayMessage = Array.isArray(data)
+ const isArrayMessage = Array.isArray(data);
+ const text = isArrayMessage ?
+ data.map(msg => msg.content).join('') :
+ data;
if (params.value.n_probs > 0 && isArrayMessage) {
message = html`<${Probabilities} data=${data} />`
} else {
- const text = isArrayMessage ?
- data.map(msg => msg.content).join('') :
- data;
message = isCompletionMode ?
text :
html`<${Markdownish} text=${template(text)} />`
}
+
+ const fromBot = user && user === '{{char}}';
+ if (fromBot && !lastCharChatLinesIxs.value.includes(index))
+ lastCharChatLinesIxs.value.push(index);
+
if (user) {
- return html`<p key=${index}><strong>${template(user)}:</strong> ${message}</p>`
+ return html`
+ <div>
+ <p key=${index}><strong>${template(user)}:</strong> ${message}</p>
+ ${
+ fromBot && ttsVoice.value
+ && html`<button disabled=${generating.value} onclick=${e => ttsChatLine(e, index, text)} aria-label=${ttsChatLineActiveIx.value === index ? 'Pause' : 'Play'}>${ ttsChatLineActiveIx.value === index ? '⏸️' : '▶️' }</div>`
+ }
+ </div>
+ `;
} else {
return isCompletionMode ?
html`<span key=${index}>${message}</span>` :
- html`<p key=${index}>${message}</p>`
+ html`<div><p key=${index}>${message}</p></div>`
}
};
diff --git a/examples/server/public/json-schema-to-grammar.mjs b/examples/server/public/json-schema-to-grammar.mjs
index faed6a32..7267f3f9 100644
--- a/examples/server/public/json-schema-to-grammar.mjs
+++ b/examples/server/public/json-schema-to-grammar.mjs
@@ -24,6 +24,201 @@ function _buildRepetition(itemRule, minItems, maxItems, opts={}) {
return minItems === 0 ? `(${result})?` : result;
}
+function _generateMinMaxInt(minValue, maxValue, out, decimalsLeft = 16, topLevel = true) {
+ const hasMin = minValue !== null;
+ const hasMax = maxValue !== null;
+
+ function digitRange(fromChar, toChar) {
+ out.push("[");
+ if (fromChar === toChar) {
+ out.push(fromChar);
+ } else {
+ out.push(fromChar);
+ out.push("-");
+ out.push(toChar);
+ }
+ out.push("]");
+ }
+
+ function moreDigits(minDigits, maxDigits) {
+ out.push("[0-9]");
+ if (minDigits === maxDigits && minDigits === 1) {
+ return;
+ }
+ out.push("{");
+ out.push(minDigits.toString());
+ if (maxDigits !== minDigits) {
+ out.push(",");
+ if (maxDigits !== Number.MAX_SAFE_INTEGER) {
+ out.push(maxDigits.toString());
+ }
+ }
+ out.push("}");
+ }
+
+ function uniformRange(fromStr, toStr) {
+ let i = 0;
+ while (i < fromStr.length && fromStr[i] === toStr[i]) {
+ i++;
+ }
+ if (i > 0) {
+ out.push("\"");
+ out.push(fromStr.slice(0, i));
+ out.push("\"");
+ }
+ if (i < fromStr.length) {
+ if (i > 0) {
+ out.push(" ");
+ }
+ const subLen = fromStr.length - i - 1;
+ if (subLen > 0) {
+ const fromSub = fromStr.slice(i + 1);
+ const toSub = toStr.slice(i + 1);
+ const subZeros = "0".repeat(subLen);
+ const subNines = "9".repeat(subLen);
+
+ let toReached = false;
+ out.push("(");
+ if (fromSub === subZeros) {
+ digitRange(fromStr[i], String.fromCharCode(toStr.charCodeAt(i) - 1));
+ out.push(" ");
+ moreDigits(subLen, subLen);
+ } else {
+ out.push("[");
+ out.push(fromStr[i]);
+ out.push("] ");
+ out.push("(");
+ uniformRange(fromSub, subNines);
+ out.push(")");
+ if (fromStr.charCodeAt(i) < toStr.charCodeAt(i) - 1) {
+ out.push(" | ");
+ if (toSub === subNines) {
+ digitRange(String.fromCharCode(fromStr.charCodeAt(i) + 1), toStr[i]);
+ toReached = true;
+ } else {
+ digitRange(String.fromCharCode(fromStr.charCodeAt(i) + 1), String.fromCharCode(toStr.charCodeAt(i) - 1));
+ }
+ out.push(" ");
+ moreDigits(subLen, subLen);
+ }
+ }
+ if (!toReached) {
+ out.push(" | ");
+ digitRange(toStr[i], toStr[i]);
+ out.push(" ");
+ uniformRange(subZeros, toSub);
+ }
+ out.push(")");
+ } else {
+ out.push("[");
+ out.push(fromStr[i]);
+ out.push("-");
+ out.push(toStr[i]);
+ out.push("]");
+ }
+ }
+ }
+
+ if (hasMin && hasMax) {
+ if (minValue < 0 && maxValue < 0) {
+ out.push("\"-\" (");
+ _generateMinMaxInt(-maxValue, -minValue, out, decimalsLeft, true);
+ out.push(")");
+ return;
+ }
+
+ if (minValue < 0) {
+ out.push("\"-\" (");
+ _generateMinMaxInt(0, -minValue, out, decimalsLeft, true);
+ out.push(") | ");
+ minValue = 0;
+ }
+
+ let minS = minValue.toString();
+ const maxS = maxValue.toString();
+ const minDigits = minS.length;
+ const maxDigits = maxS.length;
+
+ for (let digits = minDigits; digits < maxDigits; digits++) {
+ uniformRange(minS, "9".repeat(digits));
+ minS = "1" + "0".repeat(digits);
+ out.push(" | ");
+ }
+ uniformRange(minS, maxS);
+ return;
+ }
+
+ const lessDecimals = Math.max(decimalsLeft - 1, 1);
+
+ if (hasMin) {
+ if (minValue < 0) {
+ out.push("\"-\" (");
+ _generateMinMaxInt(null, -minValue, out, decimalsLeft, false);
+ out.push(") | [0] | [1-9] ");
+ moreDigits(0, decimalsLeft - 1);
+ } else if (minValue === 0) {
+ if (topLevel) {
+ out.push("[0] | [1-9] ");
+ moreDigits(0, lessDecimals);
+ } else {
+ moreDigits(1, decimalsLeft);
+ }
+ } else if (minValue <= 9) {
+ const c = minValue.toString();
+ const range_start = topLevel ? '1' : '0';
+ if (c > range_start) {
+ digitRange(range_start, String.fromCharCode(c.charCodeAt(0) - 1));
+ out.push(" ");
+ moreDigits(1, lessDecimals);
+ out.push(" | ");
+ }
+ digitRange(c, "9");
+ out.push(" ");
+ moreDigits(0, lessDecimals);
+ } else {
+ const minS = minValue.toString();
+ const length = minS.length;
+ const c = minS[0];
+
+ if (c > "1") {
+ digitRange(topLevel ? "1" : "0", String.fromCharCode(c.charCodeAt(0) - 1));
+ out.push(" ");
+ moreDigits(length, lessDecimals);
+ out.push(" | ");
+ }
+ digitRange(c, c);
+ out.push(" (");
+ _generateMinMaxInt(parseInt(minS.slice(1)), null, out, lessDecimals, false);
+ out.push(")");
+ if (c < "9") {
+ out.push(" | ");
+ digitRange(String.fromCharCode(c.charCodeAt(0) + 1), "9");
+ out.push(" ");
+ moreDigits(length - 1, lessDecimals);
+ }
+ }
+ return;
+ }
+
+ if (hasMax) {
+ if (maxValue >= 0) {
+ if (topLevel) {
+ out.push("\"-\" [1-9] ");
+ moreDigits(0, lessDecimals);
+ out.push(" | ");
+ }
+ _generateMinMaxInt(0, maxValue, out, decimalsLeft, true);
+ } else {
+ out.push("\"-\" (");
+ _generateMinMaxInt(-maxValue, null, out, decimalsLeft, false);
+ out.push(")");
+ }
+ return;
+ }
+
+ throw new Error("At least one of minValue or maxValue must be set");
+}
+
class BuiltinRule {
constructor(content, deps) {
this.content = content;
@@ -64,7 +259,7 @@ const GRAMMAR_RANGE_LITERAL_ESCAPE_RE = /[\n\r"\]\-\\]/g;
const GRAMMAR_LITERAL_ESCAPES = { '\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]' };
const NON_LITERAL_SET = new Set('|.()[]{}*+?');
-const ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = new Set('[]()|{}*+?');
+const ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = new Set('^$.[]()|{}*+?');
export class SchemaConverter {
constructor(options) {
@@ -337,6 +532,64 @@ export class SchemaConverter {
return this._addRule(name, "\"\\\"\" " + toRule(transform()) + " \"\\\"\" space")
}
+ _notStrings(strings) {
+ class TrieNode {
+ constructor() {
+ this.children = {};
+ this.isEndOfString = false;
+ }
+
+ insert(str) {
+ let node = this;
+ for (const c of str) {
+ node = node.children[c] = node.children[c] || new TrieNode();
+ }
+ node.isEndOfString = true;
+ }
+ }
+
+ const trie = new TrieNode();
+ for (const s of strings) {
+ trie.insert(s);
+ }
+
+ const charRuleName = this._addPrimitive('char', PRIMITIVE_RULES['char']);
+ const out = ['["] ( '];
+
+ const visit = (node) => {
+ const rejects = [];
+ let first = true;
+ for (const c of Object.keys(node.children).sort()) {
+ const child = node.children[c];
+ rejects.push(c);
+ if (first) {
+ first = false;
+ } else {
+ out.push(' | ');
+ }
+ out.push(`[${c}]`);
+ if (Object.keys(child.children).length > 0) {
+ out.push(' (');
+ visit(child);
+ out.push(')');
+ } else if (child.isEndOfString) {
+ out.push(` ${charRuleName}+`);
+ }
+ }
+ if (Object.keys(node.children).length > 0) {
+ if (!first) {
+ out.push(' | ');
+ }
+ out.push(`[^"${rejects.join('')}] ${charRuleName}*`);
+ }
+ };
+
+ visit(trie);
+
+ out.push(` )${trie.isEndOfString ? '' : '?'} ["] space`);
+ return out.join('');
+ }
+
_resolveRef(ref) {
let refName = ref.split('/').pop();
if (!(refName in this._rules) && !this._refsBeingResolved.has(ref)) {
@@ -363,11 +616,11 @@ export class SchemaConverter {
} else if (schema.oneOf || schema.anyOf) {
return this._addRule(ruleName, this._generateUnionRule(name, schema.oneOf || schema.anyOf));
} else if (Array.isArray(schemaType)) {
- return this._addRule(ruleName, this._generateUnionRule(name, schemaType.map(t => ({ type: t }))));
+ return this._addRule(ruleName, this._generateUnionRule(name, schemaType.map(t => ({...schema, type: t}))));
} else if ('const' in schema) {
- return this._addRule(ruleName, this._generateConstantRule(schema.const));
+ return this._addRule(ruleName, this._generateConstantRule(schema.const) + ' space');
} else if ('enum' in schema) {
- const rule = schema.enum.map(v => this._generateConstantRule(v)).join(' | ');
+ const rule = '(' + schema.enum.map(v => this._generateConstantRule(v)).join(' | ') + ') space';
return this._addRule(ruleName, rule);
} else if ((schemaType === undefined || schemaType === 'object') &&
('properties' in schema ||
@@ -404,7 +657,7 @@ export class SchemaConverter {
}
}
- return this._addRule(ruleName, this._buildObjectRule(properties, required, name, /* additionalProperties= */ false));
+ return this._addRule(ruleName, this._buildObjectRule(properties, required, name, null));
} else if ((schemaType === undefined || schemaType === 'array') && ('items' in schema || 'prefixItems' in schema)) {
const items = schema.items ?? schema.prefixItems;
if (Array.isArray(items)) {
@@ -435,6 +688,24 @@ export class SchemaConverter {
const minLen = schema.minLength || 0;
const maxLen = schema.maxLength;
return this._addRule(ruleName, '"\\\"" ' + _buildRepetition(charRuleName, minLen, maxLen) + ' "\\\"" space');
+ } else if (schemaType === 'integer' && ('minimum' in schema || 'exclusiveMinimum' in schema || 'maximum' in schema || 'exclusiveMaximum' in schema)) {
+ let minValue = null;
+ let maxValue = null;
+ if ('minimum' in schema) {
+ minValue = schema.minimum;
+ } else if ('exclusiveMinimum' in schema) {
+ minValue = schema.exclusiveMinimum + 1;
+ }
+ if ('maximum' in schema) {
+ maxValue = schema.maximum;
+ } else if ('exclusiveMaximum' in schema) {
+ maxValue = schema.exclusiveMaximum - 1;
+ }
+
+ const out = ["("];
+ _generateMinMaxInt(minValue, maxValue, out);
+ out.push(") space");
+ return this._addRule(ruleName, out.join(''));
} else if ((schemaType === 'object') || (Object.keys(schema).length === 0)) {
return this._addRule(ruleName, this._addPrimitive('object', PRIMITIVE_RULES['object']));
} else {
@@ -480,12 +751,19 @@ export class SchemaConverter {
const requiredProps = sortedProps.filter(k => required.has(k));
const optionalProps = sortedProps.filter(k => !required.has(k));
- if (typeof additionalProperties === 'object' || additionalProperties === true) {
+ if (additionalProperties) {
const subName = `${name ?? ''}${name ? '-' : ''}additional`;
- const valueRule = this.visit(additionalProperties === true ? {} : additionalProperties, `${subName}-value`);
+ const valueRule =
+ additionalProperties != null && typeof additionalProperties === 'object' ? this.visit(additionalProperties, `${subName}-value`)
+ : this._addPrimitive('value', PRIMITIVE_RULES['value']);
+
+ const key_rule =
+ sortedProps.length === 0 ? this._addPrimitive('string', PRIMITIVE_RULES['string'])
+ : this._addRule(`${subName}-k`, this._notStrings(sortedProps));
+
propKvRuleNames['*'] = this._addRule(
`${subName}-kv`,
- `${this._addPrimitive('string', PRIMITIVE_RULES['string'])} ":" space ${valueRule}`);
+ `${key_rule} ":" space ${valueRule}`);
optionalProps.push('*');
}
@@ -502,15 +780,11 @@ export class SchemaConverter {
const [k, ...rest] = ks;
const kvRuleName = propKvRuleNames[k];
let res;
- if (k === '*') {
- res = this._addRule(
- `${name ?? ''}${name ? '-' : ''}additional-kvs`,
- `${kvRuleName} ( "," space ` + kvRuleName + ` )*`
- )
- } else if (firstIsOptional) {
- res = `( "," space ${kvRuleName} )?`;
+ const commaRef = `( "," space ${kvRuleName} )`;
+ if (firstIsOptional) {
+ res = commaRef + (k === '*' ? '*' : '?');
} else {
- res = kvRuleName;
+ res = kvRuleName + (k === '*' ? ' ' + commaRef + '*' : '');
}
if (rest.length > 0) {
res += ' ' + this._addRule(