wip toolbar
This commit is contained in:
parent
0758105e92
commit
9f03721f86
|
|
@ -14,16 +14,6 @@
|
|||
#status { font-size: 12px; color: #666; margin-bottom: 10px; }
|
||||
#revisions { margin-top: 20px; }
|
||||
#revisions button { margin: 2px; }
|
||||
#ribbit { border: 1px solid #ccc; border-radius: 4px; padding: 20px; min-height: 200px; }
|
||||
.ribbit-toolbar { background: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 4px; margin-bottom: 8px; }
|
||||
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 2px; align-items: center; }
|
||||
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
|
||||
.ribbit-toolbar button:hover { background: #e8e8e8; }
|
||||
.ribbit-toolbar button.active { background: #d0d0ff; border-color: #99f; }
|
||||
.ribbit-toolbar button.disabled { opacity: 0.3; cursor: default; }
|
||||
.ribbit-toolbar .spacer { width: 12px; }
|
||||
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; border-radius: 4px; padding: 4px; z-index: 10; }
|
||||
.ribbit-dropdown button { display: block; width: 100%; text-align: left; margin: 1px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
6841
package-lock.json
generated
6841
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -32,5 +32,8 @@
|
|||
"selenium-webdriver": "^4.43.0",
|
||||
"ts-jest": "^29.4.9",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "^1.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
*/
|
||||
|
||||
.md-delim {
|
||||
display:inline;
|
||||
display:none;
|
||||
opacity: 0.3;
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
|
|
@ -46,20 +46,9 @@
|
|||
background: #EEE;
|
||||
}
|
||||
|
||||
.ribbit-editing .md-delim {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#ribbit.wysiwyg [class^="md-h"] > .md-delim,
|
||||
#ribbit.wysiwyg .md-blockquote > .md-delim,
|
||||
#ribbit.wysiwyg .md-list-prefix {
|
||||
.ribbit-editing > .md-delim {
|
||||
display: inline;
|
||||
/*
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
*/
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* List prefixes use a separate class so CSS can replace them with
|
||||
|
|
|
|||
1
src/static/themes/ribbit-default/icons
Symbolic link
1
src/static/themes/ribbit-default/icons
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../node_modules/bootstrap-icons/icons
|
||||
|
|
@ -6,6 +6,11 @@
|
|||
|
||||
@import "../../ribbit-core.css";
|
||||
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
main { max-width: 960px; margin: auto }
|
||||
|
||||
#ribbit { border: 1px solid #ccc; border-radius: 4px; padding: 20px; min-height: 200px; }
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -50,3 +55,80 @@ code {
|
|||
background: #EEE;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.ribbit-toolbar {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc; border-radius: 4px; padding: 4px; margin-bottom: 8px;
|
||||
}
|
||||
.ribbit-toolbar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ribbit-toolbar button {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 1rem 1rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.ribbit-toolbar button:hover {
|
||||
background-color: #DDD;
|
||||
background-blend-mode: darken;
|
||||
}
|
||||
.ribbit-toolbar button.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
.ribbit-btn-fencedCode { background-image: url("icons/code-square.svg"); }
|
||||
.ribbit-btn-blockquote { background-image: url("icons/blockquote-left.svg"); }
|
||||
.ribbit-btn-hr { background-image: url("icons/hr.svg"); }
|
||||
.ribbit-btn-table { background-image: url("icons/table.svg"); }
|
||||
.ribbit-btn-code { background-image: url("icons/code.svg"); }
|
||||
.ribbit-btn-link { background-image: url("icons/link.svg"); }
|
||||
.ribbit-btn-boldItalic { background-image: url("icons/type-bold.svg"); }
|
||||
.ribbit-btn-bold { background-image: url("icons/type-bold.svg"); }
|
||||
.ribbit-btn-italic { background-image: url("icons/type-italic.svg"); }
|
||||
.ribbit-btn-strikethrough { background-image: url("icons/type-strikethrough.svg"); }
|
||||
.ribbit-btn-h1 { background-image: url("icons/type-h1.svg"); }
|
||||
.ribbit-btn-h2 { background-image: url("icons/type-h2.svg"); }
|
||||
.ribbit-btn-h3 { background-image: url("icons/type-h3.svg"); }
|
||||
.ribbit-btn-h4 { background-image: url("icons/type-h4.svg"); }
|
||||
.ribbit-btn-h5 { background-image: url("icons/type-h5.svg"); }
|
||||
.ribbit-btn-h6 { background-image: url("icons/type-h6.svg"); }
|
||||
.ribbit-btn-ul { background-image: url("icons/list-ul.svg"); }
|
||||
.ribbit-btn-ol { background-image: url("icons/list-ol.svg"); }
|
||||
.ribbit-btn-edit { background-image: url("icons/pen.svg"); }
|
||||
.ribbit-btn-save { background-image: url("icons/floppy.svg"); }
|
||||
.ribbit-btn-toggle { background-image: url("icons/toggle-off.svg"); }
|
||||
|
||||
|
||||
.ribbit-toolbar .spacer {
|
||||
width: 12px;
|
||||
}
|
||||
.ribbit-dropdown {
|
||||
position: absolute;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
.ribbit-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -335,6 +335,60 @@ export class RibbitEditor extends Ribbit {
|
|||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-pass over a line of markdown text that disambiguates closing
|
||||
* runs of 3 consecutive asterisks (e.g. "**bold *italic***") into two
|
||||
* separate, unambiguous closing tokens, separated by a sentinel.
|
||||
*
|
||||
* Markdown allows *and* ** to nest (one inside the other) but never a
|
||||
* delimiter inside itself, so at most two distinct asterisk-based
|
||||
* delimiters can be open at once. A run of exactly 3 closing asterisks
|
||||
* therefore always means "close the most-recently-opened one (top of
|
||||
* stack), then close the other" — there is no other valid reading.
|
||||
*
|
||||
* disambiguateAsteriskRuns('**bold *italic***')
|
||||
* // '**bold *italic*\u200C**' (sentinel inserted between closers)
|
||||
*/
|
||||
#disambiguateAsteriskRuns(line: string): string {
|
||||
const SENTINEL = '\u200C'; // zero-width non-joiner, distinct from
|
||||
// the \u200B already used for caret placeholders
|
||||
const ASTERISK_RUN = /\*{1,3}/g;
|
||||
const stack: ('*' | '**')[] = [];
|
||||
let result = '';
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = ASTERISK_RUN.exec(line)) !== null) {
|
||||
result += line.slice(lastIndex, match.index);
|
||||
const run = match[0];
|
||||
|
||||
if (run.length === 3 && stack.length === 2) {
|
||||
// Ambiguous: split into [top-of-stack closer][sentinel][remaining closer]
|
||||
const innerCloser = stack.pop()!; // most recently opened
|
||||
const outerCloser = stack.pop()!; // closes after inner
|
||||
result += innerCloser + SENTINEL + outerCloser;
|
||||
} else if (run.length === 3) {
|
||||
// *** as a single atomic bold-italic token (open or close,
|
||||
// not part of an ambiguous nested pair) — pass through.
|
||||
result += run;
|
||||
} else if (stack.length > 0 && stack[stack.length - 1] === run) {
|
||||
// Closing the most recently opened delimiter of this exact length
|
||||
stack.pop();
|
||||
result += run;
|
||||
} else {
|
||||
// Opening a new delimiter
|
||||
stack.push(run as '*' | '**');
|
||||
result += run;
|
||||
}
|
||||
|
||||
lastIndex = match.index + run.length;
|
||||
}
|
||||
|
||||
result += line.slice(lastIndex);
|
||||
console.log(`DEBUG:\n input: ${line}\n output: ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single styled block <div> from one markdown line.
|
||||
* Classifies the line, wraps the block prefix in a .md-delim span,
|
||||
|
|
@ -430,6 +484,7 @@ export class RibbitEditor extends Ribbit {
|
|||
* this.#parseInline('hello **world** and `code`')
|
||||
*/
|
||||
#parseInline(text: string): DocumentFragment {
|
||||
|
||||
const classes: Record<string, string> = {
|
||||
'***': 'md-bold-italic',
|
||||
'**': 'md-bold',
|
||||
|
|
@ -450,6 +505,8 @@ export class RibbitEditor extends Ribbit {
|
|||
const fragment = document.createDocumentFragment();
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
text = this.#disambiguateAsteriskRuns(text);
|
||||
|
||||
while ((match = INLINE_PATTERN.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const MACRO_ID_PREFIX = 'macro:';
|
|||
const DROPDOWN_INDICATOR = ' ▾';
|
||||
|
||||
/** IDs of buttons that belong in the utility section, not the tag/macro area. */
|
||||
const UTILITY_BUTTON_IDS = ['save', 'toggle', 'markdown'];
|
||||
const UTILITY_BUTTON_IDS = ['save', 'edit'];
|
||||
|
||||
const MAX_HEADING_LEVEL = 6;
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ export class ToolbarManager {
|
|||
action: 'custom',
|
||||
handler: () => this.editor.save(),
|
||||
});
|
||||
this.register('toggle', {
|
||||
this.register('edit', {
|
||||
label: 'Edit',
|
||||
shortcut: 'Ctrl+Shift+V',
|
||||
action: 'custom',
|
||||
|
|
@ -229,18 +229,6 @@ export class ToolbarManager {
|
|||
}
|
||||
},
|
||||
});
|
||||
this.register('markdown', {
|
||||
label: 'Source',
|
||||
shortcut: 'Ctrl+/',
|
||||
action: 'custom',
|
||||
handler: () => {
|
||||
if (this.editor.getState() === EDITOR_STATE_EDIT) {
|
||||
this.editor.wysiwyg();
|
||||
} else {
|
||||
this.editor.edit();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -314,7 +302,7 @@ export class ToolbarManager {
|
|||
items: macroIds,
|
||||
});
|
||||
}
|
||||
slots.push('', 'markdown', 'save', 'toggle');
|
||||
slots.push('', 'save', 'edit');
|
||||
return slots;
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +418,7 @@ export class ToolbarManager {
|
|||
const listItem = document.createElement('li');
|
||||
const buttonElement = document.createElement('button');
|
||||
buttonElement.className = `ribbit-btn-${button.id}`;
|
||||
buttonElement.textContent = button.label;
|
||||
//buttonElement.textContent = button.label;
|
||||
buttonElement.setAttribute('aria-label', button.label);
|
||||
buttonElement.title = button.shortcut
|
||||
? `${button.label} (${button.shortcut})`
|
||||
|
|
|
|||
|
|
@ -4,29 +4,6 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Ribbit Integration Test Page</title>
|
||||
<link rel="stylesheet" href="/static/themes/ribbit-default/theme.css">
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
main { max-width: 960px; margin: auto }
|
||||
#ribbit {
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
min-height: 200px;
|
||||
margin: auto;
|
||||
}
|
||||
.ribbit-toolbar {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 2px; }
|
||||
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
|
||||
.ribbit-toolbar button.active { background: #d0d0ff; }
|
||||
.ribbit-toolbar button.disabled { opacity: 0.3; }
|
||||
.ribbit-toolbar .spacer { width: 12px; }
|
||||
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; padding: 4px; }
|
||||
.ribbit-dropdown button { display: block; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user