diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 90d6cb815..a4a92f083 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -2,10 +2,9 @@ name: Create draft release
on:
push:
- branches-ignore:
- - '2.3-maintenance'
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
+ - '!2.3.*'
jobs:
build:
@@ -13,17 +12,16 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v2
with:
+ cache: npm
node-version: 14
- name: Install
run: npm install
- name: Test
run: npm run test
- # Disable the CI flag to allow warnings in the build.
- # https://github.com/facebook/create-react-app/issues/3657
- name: Build
- run: CI=false npm run build
+ run: npm run build
- name: Create draft release
uses: softprops/action-gh-release@v1
with:
diff --git a/.github/workflows/prs.yml b/.github/workflows/prs.yml
index d6d413b3f..997f34dbc 100644
--- a/.github/workflows/prs.yml
+++ b/.github/workflows/prs.yml
@@ -11,8 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/setup-node@v2
with:
+ cache: npm
node-version: 14
- name: Install
run: npm install
diff --git a/EXTENDING.md b/EXTENDING.md
new file mode 100644
index 000000000..fc0caf62e
--- /dev/null
+++ b/EXTENDING.md
@@ -0,0 +1,481 @@
+# Extending Twine
+
+## Introduction
+
+As of version 2.4, story formats can extend Twine's user interface in specific
+ways:
+
+- A format may add a CodeMirror syntax highlighting mode to the passage editor.
+- A format may add custom CodeMirror commands and a toolbar which triggers them,
+ which also appears above the passage editor.
+- A format may add a _reference parser_, which causes connections to appear
+ between passages.
+
+This document explains these capabilities and how a story format author
+can use them.
+
+- Creating a standalone extension for Twine is not possible. Extensions can only
+ be bundled with a story format.
+- Users can choose to disable Twine extensions for a story format. For this
+ reason (and also because users might use other compilers and editors with a
+ story format), Twine extensions should never be essential for use of a story
+ format.
+- Although extensions are part of a story format file, they will not affect the
+ size of stories compiled with that format.
+
+This document does its best to document the behavior of Twine regarding
+extensions as completely as it can. However, it may contain mistakes or be
+incomplete in places. If your extension makes use of behavior, intentional or
+not, that exists in Twine that isn't documented here, then it may break in
+future versions of Twine with no warning.
+
+Before continuing, please read the [Twine 2 story format specs][format-specs].
+These explain the basics of how Twine 2 story formats work.
+
+## Hydration
+
+Story formats are encoded in JSONP format:
+
+```javascript
+window.storyFormat({"name": "My Story Format", "version": "1.0.0", "source": "..."});
+```
+
+JSONP is itself a thin wrapper over JSON, which does not permit executable code
+to be encoded. Instead, Twine 2.4 and later uses a `hydrate` property that
+allows for a format to add JavaScript functions or other data types not allowed
+by JSON after being loaded. Below is an example of a simple `hydrate` property:
+
+```javascript
+window.storyFormat({
+ "name": "My StoryFormat",
+ "version": "1.0.0",
+ "hydrate": "this.hydratedProperty = () => console.log('Hello Twine');"
+});
+```
+
+When Twine loads this format, it creates a JavaScript function from the source
+of the `hydrate` property, then executes it, binding `this` to an empty
+JavaScript object. The hydrate property can add whatever properties it would
+like, which will be merged with the story format properties specified in JSONP.
+In the example above, a function called `hydratedProperty` would be added to the
+story format object.
+
+- The `hydrate` property may not contain any asynchronous code. It may add
+ properties that are themselves asynchronous functions, but the `hydrate`
+ property itself must be synchronous.
+- Check the `browserslist` property of [package.json](package.json) to see what
+ version of JavaScript Twine supports.
+- Only use `hydrate` to add properties that can't be represented in JSONP.
+- The `hydrate` property must not contain any side effects, because it may be
+ called repeatedly by Twine. It should not change the DOM, affect the global
+ JavaScript scope, or otherwise do anything but define properties on `this`.
+- Any properties added through `hydrate` must not conflict with properties
+ specified in JSONP. Twine will ignore these and use what is in the JSONP.
+
+You almost certainly will want to use tools to create the `hydrate` property,
+similar to how you would compile the `source` property of your format. [An
+example repo](story-format-starter) is available demonstrating how to do this
+with Webpack 5. The [`--context` option of
+Rollup](https://www.rollupjs.org/guide/en/#context) can also be used to bundle
+code.
+
+For clarity's sake, code examples in this document show story formats after they
+have been hydrated.
+
+## Versioning
+
+The way extensions work in Twine may change in future versions (and probably
+will). So that story format authors don't need to publish multiple versions as
+Twine changes, Twine's editor extensions are stored under a version specifier:
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "anExtensionProperty": "red"
+ },
+ "^3.0.0": {
+ "anExtensionProperty": "blue"
+ }
+ }
+ }
+});
+```
+
+In this story format, Twine 2.4.0-alpha1 and later would see
+`anExtensionProperty` of `"red"`, but Twine 3.0 (as an example--at time of
+writing, this version doesn't exist) would see it as `"blue"`.
+
+- Twine follows [semantic versioning].
+- Twine uses the `satisfies()` function of the [semver NPM package] to decide if
+ a specifier matches the current Twine version. You may use any specifier that
+ this function understands.
+- You should never have overlapping version specifiers in your extensions. If
+ multiple object keys satisfy the version of Twine, then Twine will issue a
+ warning and use the first it finds that matched. From a practical standpoint,
+ this means that its behavior is dependent on the browser or platform it's
+ running on, and cannot be predicted.
+- If no object keys satisfy the version of Twine, then no extensions will be
+ used.
+- Only the object keys for a specific story format version will be used. If only
+ version 2.0.0 of a story format contains extensions but the user is using
+ version 1.0.0, no extensions will be used.
+
+## CodeMirror Syntax Highlighting
+
+Twine uses [CodeMirror] in its passage editor, which allows modes to be defined
+which apply formatting to source code. A story format can define its own mode
+to, for example, highlight special instructions that the story format accepts.
+
+A mode is defined using a hydrated function:
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "codeMirror": {
+ mode() {
+ return {
+ startState() {
+ return {};
+ },
+ token(stream, state) {
+ stream.skipToEnd();
+ return 'keyword';
+ }
+ };
+ }
+ }
+ }
+ }
+ }
+});
+```
+
+This example would mark the entire passage text as a `keyword` token.
+
+- See [CodeMirror's syntax mode documentation] for details on what the `mode`
+ function should return, and how to parse passage text. The function specified
+ here will be used as the second argument to `CodeMirror.defineMode()`.
+- Specifically, as the documentation notes, your CodeMirror mode must not modify
+ the global scope in any way.
+- Twine manages the name of your syntax mode and sets the passage editor's
+ CodeMirror instance accordingly. You must not rely on the name of the syntax
+ mode being formatted in a particular way. This is to prevent one story format
+ from interfering with another's functioning.
+- You must use CodeMirror's built-in tokens. Twine contains styling for these
+ tokens that will adapt to the user-selected theme (e.g. light or dark). It
+ doesn't appear that CodeMirror contains documentation for what these are apart
+ from [its own CSS](codemirror-css), unfortunately. The section commented
+ `DEFAULT THEME` lists available ones.
+- A future version of Twine might allow custom tokens and appearance.
+- A story format mode has no access to the story that the passage belongs to. It
+ must parse the text on its own terms.
+
+## CodeMirror Commands and Toolbar
+
+An editor toolbar can be specified by a format, which will appear between the
+built-in one and the passage text. A toolbar must specify custom CodeMirror
+commands that are triggered by buttons in the toolbar.
+
+- A format can only have one toolbar.
+- A toolbar can only use CodeMirror commands defined by the format. These
+ commands can consist of any code, however, which in turn may call other
+ CodeMirror commands or otherwise do whatever it likes.
+
+## CodeMirror Toolbar
+
+A CodeMirror toolbar is specified through a hydrated function:
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "codeMirror": {
+ toolbar(editor, environment) {
+ return [
+ {
+ type: 'button',
+ command: 'customCommand',
+ icon: 'data:image/svg+xml,...',
+ label: 'Custom Command'
+ },
+ {
+ type: 'menu',
+ icon: 'data:image/svg+xml,...',
+ label: 'Menu',
+ items: [
+ {
+ type: 'button',
+ command: 'customCommand2',
+ disabled: true,
+ icon: 'data:image/svg+xml,...',
+ label: 'Custom Commmand 2'
+ },
+ {
+ type: 'separator'
+ },
+ {
+ type: 'button',
+ command: 'customCommand3',
+ icon: 'data:image/svg+xml,...',
+ label: 'Custom Command 3'
+ }
+ ]
+ }
+ ];
+ }
+ }
+ }
+ }
+ }
+});
+```
+
+The `toolbar` function receives two arguments from Twine:
+
+- `editor` is the CodeMirror editor instance the toolbar is attached to. This is
+ provided so that the toolbar can enable or disable items appropriately--for
+ example, based on whether the user has selected any text. See [the CodeMirror
+ API documentation](https://codemirror.net/doc/manual.html#api) for methods and
+ properties available on this object. As the documentation indicates, you may
+ use methods that start with either `doc` or `cm`; for example,
+ `editor.getSelection()`.
+- `environment` is an object with information related to Twine itself:
+
+ - `environment.appTheme` is either the string `dark` or `light`, depending on
+ the current app theme used. If the user has chosen to have the Twine app
+ theme match the system theme, then this will reflect the current system
+ theme. This property is provided so that the toolbar can vary its icons
+ based on the app theme.
+ - `environment.locale` is a string value containing the user-set locale. If
+ the user has chosen to have the Twine app use the system locale, this value
+ will reflect that as well. This property is provided so that the toolbar can
+ localize button and menu labels.
+
+It must follow these rules:
+
+- The toolbar function must be side-effect free. Twine will call it repeatedly
+ while the user is working. It should only return what the toolbar should be
+ given the state passed to it. It must never change the CodeMirror editor
+ passed to it. Changes should occur in toolbar commands instead (described
+ below).
+- Changing `environment` properties has no effect.
+- Do not change the order of toolbar items returned based on
+ `environment.locale` (e.g. reverse the order for right-to-left locales). Twine
+ will handle this for you.
+- Avoid changing the contents of the toolbar based on CodeMirror state. Instead,
+ enable and disable items.
+- If you would like to use a built-in CodeMirror command in your toolbar, write
+ a custom command that calls `editor.execCommand()`.
+- The toolbar function must return an array of objects. The `type` property on
+ the object describes what kind of item to display. This property is required
+ on all items.
+
+### type: 'button'
+
+This displays a button which runs a CodeMirror command. Other properties:
+
+| Property Name | Type | Required | Description |
+| ------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
+| `command` | string | yes | The name of the CodeMirror command to run when |
+| `disabled` | boolean | no | If true, then the button is disabled. By default, buttons are enabled. |
+| `icon` | string | depends on context | Used as the `src` attribute for the tag used in the button. Using a `data:` URL is recommended but not required. |
+| `label` | string | yes | The label text to display. This cannot contain HTML. |
+
+The `icon` property is required if this toolbar item is not inside a menu.
+Inside of a menu, the `icon` property is forbidden.
+
+### type: 'menu'
+
+This displays a drop-down menu. Other properties:
+
+| Property Name | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `disabled` | boolean | no | If true, then the button is disabled. By default, menus are enabled. |
+| `icon` | string | yes | Used as the `src` attribute for the tag used in the button. Using a `data:` URL is recommended but not required. |
+| `items` | array | yes | Items in this menu. |
+| `label` | string | yes | The label text to display. This cannot contain HTML. |
+
+The `items` property must only contain objects with type `button` or `separator`.
+
+### type: 'separator'
+
+This displays a separator line in a menu. This type of item has no other
+properties, and is only allowed in the `items` property of a menu item.
+
+## CodeMirror Commands
+
+The commands used by a CodeMirror toolbar are specified through hydrated functions:
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "codeMirror": {
+ "commands": {
+ customCommand1(editor) {
+ editor.getDoc().replaceSelection('Example text');
+ },
+ customCommand2(editor) {
+ const doc = editor.getDoc();
+
+ doc.replaceSelection(doc.getSelection().toUpperCase());
+ }
+ }
+ }
+ }
+ }
+});
+```
+
+- When invoking a command from a story format toolbar, the name of the command
+ must match exactly, including case.
+- Twine namespaces your commands (and their connections to the toolbar) so that
+ commands specified by one story format do not interfere with another story
+ format's, or the commands of a different version of that story format. You
+ must not rely on the name assigned to your commands by Twine, as this
+ namespacing may change in future versions. A possible way for story format
+ commands to reference each other using the `hydrate` property is:
+
+```javascript
+function customCommand1(editor) {
+ editor.getDoc().replaceSelection('Example text');
+}
+
+function customCommand2(editor) {
+ const doc = editor.getDoc();
+
+ doc.replaceSelection(doc.getSelection().toUpperCase());
+ customCommand1(editor);
+}
+
+this.codeMirror = {
+ commands: {customCommand1, customCommand2}
+};
+```
+
+## Parsing References
+
+A story format can define _references_ in a story. References are secondary
+connections between two passages. Unlike links:
+
+- References to nonexistent passages do not show a broken link line in the story
+ map.
+- New passages are not automatically created for users when they add a new
+ reference in passage text.
+- Renaming a passage does not affect passage text that contains a reference to
+ that passage. (e.g. doing a find/replace for text)
+- References are drawn in Twine using a dotted line, though this appearance may
+ change in future versions.
+- Twine does not parse any references in itself. References are reserved for
+ story format use.
+
+In order to use references, a story format must define a parser through a hydrated function:
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "references": {
+ parsePassageText(text) {
+ return text.split(/\s/);
+ }
+ }
+ }
+ }
+});
+```
+
+The `parsePassageText` function must:
+
+- Be synchronous.
+- Return an array of strings, each string being the name of a passage being referred to in this text.
+- Return an empty array if there are no references in the text.
+- Avoid returning duplicate results; e.g. if some text contains multiple
+ references to the same passage, only one array item for that passage should be
+ returned. Including duplicates will not cause an error in Twine, but it will
+ slow it down.
+- Have no side effects. It will be called repeatedly by Twine.
+
+## All Together
+
+Below is an example of a hydrated story format demonstrating all editor extensions available.
+
+```javascript
+window.storyFormat({
+ "name": "My Story Format",
+ "version": "1.0.0",
+ "editorExtensions": {
+ "twine": {
+ "^2.4.0-alpha1": {
+ "codeMirror": {
+ "commands": {
+ upperCase(editor) {
+ const doc = editor.getDoc();
+
+ doc.replaceSelection(doc.getSelection().toUpperCase());
+ }
+ },
+ mode() {
+ return {
+ startState() {
+ return {};
+ },
+ token(stream, state) {
+ stream.skipToEnd();
+ return 'keyword';
+ }
+ };
+ },
+ toolbar(editor, environment) {
+ return [
+ {
+ type: 'button',
+ command: 'upperCase',
+ icon: 'data:image/svg+xml,...',
+ label: 'Uppercase Text'
+ }
+ ];
+ }
+ },
+ "references": {
+ parsePassageText(text) {
+ return text.match(/--.*?--/g);
+ }
+ }
+ }
+ }
+ }
+});
+```
+
+This specifies:
+
+- A CodeMirror mode which marks the entire passage as a keyword.
+- A toolbar with a single command, "Uppercase Text".
+- A reference parser that treats all text surrounded by two dashes (`--`) as a
+ reference.
+
+[format-specs]: https://github.com/iftechfoundation/twine-specs
+[semver npm package]: https://docs.npmjs.com/cli/v6/using-npm/semver
+[semantic versioning]: https://semver.org
+[codemirror]: https://codemirror.net
+[codemirror's syntax mode documentation]: https://codemirror.net/doc/manual.html#modeapi
+[codemirror-css]: https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css
+[story-format-starter]: https://github.com/klembot/twine-2-story-format-starter
diff --git a/package.json b/package.json
index 29806ad74..e53972e33 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Twine",
- "version": "2.4.0-alpha1",
+ "version": "2.4.0-beta1",
"description": "a GUI for creating nonlinear stories",
"author": "Chris Klimas ",
"license": "GPL-3.0",
diff --git a/public/locales/en-US.json b/public/locales/en-US.json
index a87e38cf7..ed40cd5e1 100644
--- a/public/locales/en-US.json
+++ b/public/locales/en-US.json
@@ -104,6 +104,7 @@
"loadingFormat": "Loading this story format...",
"loadError": "This story format could not be loaded ({{errorMessage}}).",
"name": "{{name}} {{version}}",
+ "useEditorExtensions": "Use Editor Extensions",
"useFormat": "Use As Default Story Format",
"useProofingFormat": "Use As Proofing Format"
},
diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json
index 664dc9ae3..6fb69c211 100644
--- a/public/locales/pt-pt.json
+++ b/public/locales/pt-pt.json
@@ -1,106 +1,336 @@
{
- "colors": {},
- "common": {
- "add": "Adicionar",
- "appName": "Twine",
- "cancel": "Cancelar",
- "delete": "Apagar",
- "duplicate": "Duplicar",
- "edit": "Editar",
- "ok": "OK",
- "play": "Jogar",
- "rename": "Mudar o nome",
- "remove": "Remover",
- "skip": "Saltar",
- "storyFormat": "Formato de história",
- "tag": "Etiqueta",
- "test": "Testar",
- "undo": "Desfazer"
- },
- "components": {
- "addStoryFormatButton": {},
- "addTagButton": {},
- "fontSelect": {"fonts": {}},
- "indentButtons": {},
- "localStorageQuota": {},
- "renamePassageButton": {"emptyName": "Indique um nome, por favor."},
- "renameStoryButton": {"emptyName": "Indique um nome, por favor."},
- "safariWarningCard": {},
- "storyCard": {},
- "storyFormatCard": {},
- "storyFormatSelect": {},
- "tagEditor": {}
- },
- "dialogs": {
- "aboutTwine": {"donateToTwine": "Ajude o Twine a crescer com uma doação"},
- "appDonation": {"noThanks": "Não, obrigado"},
- "appPrefs": {"language": "Idioma"},
- "passageEdit": {},
- "passageTags": {},
- "storyInfo": {"stats": {"title": "Estatísticas da história"}},
- "storyJavaScript": {
- "explanation": "Qualquer código de JavaScript aqui introduzido vai correr assim que a história for aberta no navegador."
- },
- "storySearch": {
- "title": "Encontrar e Substituir",
- "replaceWith": "Substituir por"
- },
- "storyStylesheet": {
- "explanation": "Qualquer fragmento de código CSS introduzido aqui irá alterar a aparência padrão da sua história."
- },
- "storyTags": {}
- },
- "electron": {
- "errors": {"storyFileChangedExternally": {}},
- "menuBar": {"edit": "Editar"},
- "storiesDirectoryName": "Histórias"
- },
- "routes": {
- "storyEdit": {
- "topBar": {
- "addPassage": "Passagem",
- "editJavaScript": "Editar o código JavaScript da história",
- "editStylesheet": "Editar a folha de estilos da história",
- "findAndReplace": "Encontrar e Substituir",
- "proofStory": "Ver uma cópia para revisão",
- "publishToFile": "Publicar como ficheiro"
- }
- },
- "storyFormatList": {
- "title": {},
- "storyFormatExplanation": "Os formatos de história controlam a aparência e o comportamento das histórias durante o jogo."
- },
- "storyImport": {},
- "storyList": {
- "noStories": "Neste momento, não há histórias gravadas no Twine. Para começar, crie uma nova história ou importe uma de um ficheiro.",
- "titleGeneric": "Histórias",
- "topBar": {
- "about": "Sobre o Twine",
- "archive": "Arquivo",
- "createStory": "História",
- "help": "Ajuda",
- "sortName": "Nome",
- "storyFormats": "Formatos"
- }
- },
- "welcome": {
- "autosaveTitle": "O seu trabalho é guardado automaticamente.",
- "doneTitle": "E é isto!",
- "gotoStoryList": "Ir para a Lista de Histórias",
- "greetingTitle": "Olá!",
- "tellMeMore": "Quero saber mais",
- "helpTitle": "É a primeira vez aqui?"
- }
- },
- "store": {
- "errors": {},
- "passageDefaults": {
- "name": "Passagem sem título",
- "textClick": "Faça duplo-clique nesta passagem para editá-la.",
- "textTouch": "Toque nesta passagem e, em seguida, no ícone do lápis para editá-la."
- },
- "storyDefaults": {"name": "História sem título"},
- "storyFormatDefaults": {"name": "Formato de história sem título"}
- },
- "undoChange": {"replaceAllText": "Substituir tudo"}
+ "colors": {
+ "blue": "Azul",
+ "green": "Verde",
+ "none": "Sem cor",
+ "orange": "Laranja",
+ "purple": "Roxo",
+ "red": "Vermelho",
+ "yellow": "Amarelo"
+ },
+ "common": {
+ "add": "Adicionar",
+ "appName": "Twine",
+ "cancel": "Cancelar",
+ "close": "Fechar",
+ "color": "Cor",
+ "custom": "Personalizado",
+ "delete": "Apagar",
+ "deleteCount": "Apagar ({{count}})",
+ "duplicate": "Duplicar",
+ "edit": "Editar",
+ "editCount": "Editar ({{count}})",
+ "import": "Importar",
+ "more": "Mais",
+ "next": "Próximo",
+ "ok": "OK",
+ "play": "Jogar",
+ "preferences": "Preferências",
+ "publishToFile": "Publicar como ficheiro",
+ "redo": "Refazer",
+ "redoChange": "Refazer {{change}}",
+ "remove": "Remover",
+ "rename": "Mudar o nome",
+ "renamePrompt": "Que novo nome queres dar a “{{name}}”?",
+ "skip": "Saltar",
+ "storyFormat": "Formato de história",
+ "tag": "Etiqueta",
+ "test": "Testar",
+ "undo": "Desfazer",
+ "undoChange": "Desfazer {{change}}"
+ },
+ "components": {
+ "addStoryFormatButton": {
+ "addPreview": "O {{storyFormatName}} {{storyFormatVersion}} vai ser adicionado.",
+ "alreadyAdded": "O {{storyFormatName}} {{storyFormatVersion}} já foi adicionado.",
+ "fetchError": "Ocorreu um erro ao transferir este formato ( {{errorMessage}} ).",
+ "invalidUrl": "O endereço que introduziste não é um URL válido.",
+ "prompt": "Para adicionares um formato de história, introduz o seu endereço em baixo."
+ },
+ "addTagButton": {
+ "addLabel": "Adicionar etiqueta",
+ "alreadyAdded": "Esta etiqueta já foi adicionada.",
+ "invalidName": "Por favor, introduz um nome válido para a etiqueta.",
+ "newTag": "Nova Etiqueta",
+ "tagColorLabel": "Cor da etiqueta",
+ "tagNameLabel": "Nome da etiqueta"
+ },
+ "fontSelect": {
+ "customFamilyDetail": "Por favor, introduz apenas o nome da fonte.",
+ "customScaleDetail": "Por favor, introduz apenas uma percentagem.",
+ "familyEmpty": "Por favor, introduz um nome de fonte.",
+ "font": "Fonte",
+ "fontSize": "Tamanho da Fonte",
+ "fonts": {
+ "monospaced": "Monoespaçado",
+ "serif": "Serifa",
+ "system": "Sistema"
+ },
+ "percentage": "{{percent}}%",
+ "percentageIsntNumber": "Por favor, introduz um número.",
+ "percentageNotPositive": "Introduz, por favor, um número maior do que 0."
+ },
+ "indentButtons": {
+ "indent": "Indentar",
+ "unindent": "Retirar indentação"
+ },
+ "localStorageQuota": {
+ "measureAgain": "Avaliar o espaço disponível novamente",
+ "percentAvailable": "{percent}% de espaço disponível"
+ },
+ "renamePassageButton": {
+ "emptyName": "Indica um nome, por favor.",
+ "nameAlreadyUsed": "A história já tem uma passagem com esse nome."
+ },
+ "renameStoryButton": {
+ "emptyName": "Indica um nome, por favor.",
+ "nameAlreadyUsed": "Já há uma história com esse nome."
+ },
+ "safariWarningCard": {
+ "addToHomeScreen": "Adiciona esta página ao teu ecrã inicial para evitares esta limitação.",
+ "archiveAndUseAnotherBrowser": "Arquiva as tuas histórias e usa outra plataforma, por favor.",
+ "howToAddToHomeScreen": "Como é que eu adiciono isto ao meu ecrã inicial?",
+ "learnMore": "Saber mais",
+ "message": "O navegador que estás a usar vai apagar todas as tuas histórias se passares sete dias sem visitar este site."
+ },
+ "storyCard": {
+ "lastUpdated": "Última edição em {{date}}",
+ "passageCount": "1 passagem",
+ "passageCount_plural": "{{count}} passagens"
+ },
+ "storyFormatCard": {
+ "author": "por {{author}}",
+ "license": "Licença: {{license}}",
+ "loadError": "Este formato de história não pôde ser carregado ( {{errorMessage}} ).",
+ "loadingFormat": "A carregar este formato de história...",
+ "name": "{{version}} {{name}}",
+ "useFormat": "Usar como formato de história por defeito",
+ "useProofingFormat": "Usar como formato de revisão"
+ },
+ "storyFormatSelect": {
+ "loadingCount": "A carregar 1 formato de história ...",
+ "loadingCount_plural": "A carregar {{loadingCount}} formatos de história..."
+ },
+ "tagEditor": {
+ "alreadyExists": "Já existe uma etiqueta com esse nome."
+ }
+ },
+ "dialogs": {
+ "aboutTwine": {
+ "codeHeader": "Código",
+ "codeRepo": "Visitar o repositório do código-fonte",
+ "donateToTwine": "Ajuda o Twine a crescer com uma doação",
+ "license": "Esta aplicação é lançada de acordo com a licença GPL v3 , mas qualquer trabalho criado com ela, pode ser lançado sob quaisquer termos, incluindo comerciais.",
+ "localizationHeader": "Localizações",
+ "title": "Sobre o {{version}}",
+ "twineDescription": "O Twine é uma aplicação de código-fonte aberto para contar histórias interativas e não lineares."
+ },
+ "appDonation": {
+ "donate": "Doar para o desenvolvimento do Twine",
+ "noThanks": "Não, obrigado",
+ "onlyOnce": "(Esta mensagem só te será apresentada uma vez. Se quiseres fazer uma doação para o desenvolvimento do Twine, há um \"link\" na caixa de diálogo \"Sobre o Twine\".)",
+ "supportMessage": "Se não podes viver sem o Twine, talvez o possas ajudar a crescer, fazendo uma doação. O Twine é um projeto de código-fonte aberto que será sempre gratuito — e graças à tua ajuda, o Twine poderá continuar a prosperar.",
+ "title": "Apoiar o desenvolvimento do Twine"
+ },
+ "appPrefs": {
+ "codeEditorFont": "Fonte do editor de código",
+ "codeEditorFontScale": "Tamanho da fonte do editor de código",
+ "fontExplanation": "Alterar a fonte aqui, afeta apenas o editor do Twine. A fonte usada na história não será alterada.",
+ "language": "Idioma",
+ "passageEditorFont": "Fonte do editor de passagem",
+ "passageEditorFontScale": "Tamanho da fonte do editor de passagens",
+ "theme": "Tema",
+ "themeDark": "Escuro",
+ "themeLight": "Claro",
+ "themeSystem": "Sistema",
+ "title": "Preferências"
+ },
+ "passageEdit": {
+ "setAsStart": "Começar a história aqui",
+ "size": "Tamanho",
+ "sizeLarge": "Larga",
+ "sizeSmall": "Pequeno",
+ "sizeTall": "Estreita",
+ "sizeWide": "Larga"
+ },
+ "passageTags": {
+ "noTags": "Não foram adicionadas etiquetas às passagens desta história.",
+ "title": "Etiquetas da passagem"
+ },
+ "storyInfo": {
+ "setStoryFormat": "Definir o formato de história",
+ "snapToGrid": "Ajustar à grelha",
+ "stats": {
+ "brokenLinks": "Links quebrados",
+ "characters": "Personagens",
+ "ifid": "O IFID desta história é {{ifid}}.",
+ "ifidExplanation": "O que é um IFID?",
+ "lastUpdate": "Esta história foi alterada pela última vez em {{date}} .",
+ "links": "Links",
+ "passages": "Passagens",
+ "title": "Estatísticas da história",
+ "words": "Palavras"
+ },
+ "storyFormatExplanation": "O que é um formato de história?"
+ },
+ "storyJavaScript": {
+ "explanation": "Qualquer código de JavaScript aqui introduzido vai correr assim que a história for aberta no navegador.",
+ "title": "JavaScript da História"
+ },
+ "storySearch": {
+ "find": "Encontrar",
+ "includePassageNames": "Incluir os nomes das passagens",
+ "matchCase": "Sensível a maiúsculas/minúsculas",
+ "matchCount": "{{count}} passagem correspondente",
+ "matchCount_plural": "{{count}} passagens correspondentes",
+ "noMatches": "Sem passagens correspondentes",
+ "replaceAll": "Substituir em todas as passagens",
+ "replaceWith": "Substituir por",
+ "title": "Encontrar e Substituir",
+ "useRegexes": "Usar expressões regulares"
+ },
+ "storyStylesheet": {
+ "explanation": "Qualquer fragmento de código CSS introduzido aqui irá alterar a aparência padrão da sua história.",
+ "title": "Folha de estilos da história"
+ },
+ "storyTags": {
+ "noTags": "Não foram adicionadas etiquetas às tuas histórias.",
+ "title": "Etiquetas de história"
+ }
+ },
+ "electron": {
+ "backupsDirectoryName": "Cópias de segurança",
+ "errors": {
+ "jsonSave": "Ocorreu um erro ao gravar o ficheiro de configurações.",
+ "storyDelete": "Ocorreu um erro ao apagar a história.",
+ "storyFileChangedExternally": {
+ "detail": "As alterações vão ser gravadas por cima deste ficheiro. Se quiseres usar este ficheiro em vez da versão que está no Twine, o Twine vai reiniciar, e teu trabalho será substituído pelo do ficheiro.",
+ "message": "O ficheiro “ {{fileName}} ” da tua biblioteca de histórias foi alterado fora do Twine.",
+ "overwriteChoice": "Gravar alterações no Twine",
+ "relaunchChoice": "Usar o ficheiro e reiniciar"
+ },
+ "storyRename": "Ocorreu um erro ao renomear a história.",
+ "storySave": "Ocorreu um erro ao gravar a história."
+ },
+ "menuBar": {
+ "edit": "Editar",
+ "showDevTools": "Mostrar consola de depuração",
+ "showStoryLibrary": "Mostrar a biblioteca de histórias",
+ "speech": "Fala",
+ "troubleshooting": "Solução de problemas",
+ "twineHelp": "Ajuda do Twine",
+ "view": "Visualizar"
+ },
+ "storiesDirectoryName": "Histórias"
+ },
+ "routes": {
+ "storyEdit": {
+ "topBar": {
+ "addPassage": "Passagem",
+ "editJavaScript": "Editar o código JavaScript da história",
+ "editStylesheet": "Editar a folha de estilos da história",
+ "findAndReplace": "Encontrar e substituir",
+ "passageTags": "Editar Etiquetas da Passagem",
+ "proofStory": "Ver uma cópia para revisão",
+ "publishToFile": "Publicar como ficheiro",
+ "selectAllPassages": "Marcar todas as passagens",
+ "storyInfo": "Informações sobre a história",
+ "zoomIn": "Aumentar zoom",
+ "zoomOut": "Reduzir o zoom"
+ }
+ },
+ "storyFormatList": {
+ "noneVisible": "Nenhum formato de história corresponde aos critérios que selecionaste.",
+ "show": "Mostrar...",
+ "storyFormatExplanation": "Os formatos de história controlam a aparência e o comportamento das histórias durante o jogo.",
+ "title": {
+ "all": "Todos os formatos de história",
+ "current": "Formatos de história disponíveis",
+ "user": "Formatos de história adicionados pelo utilizador"
+ }
+ },
+ "storyImport": {
+ "choosePrompt": "Escolhe as histórias que queres importar do arquivo que carregaste:",
+ "deselectAll": "Desmarcar tudo",
+ "importDifferentFile": "Importar um ficheiro diferente",
+ "importSelected": "Importar os ficheiros selecionados",
+ "importThisStory": "Importar esta história",
+ "noStoriesInFile": "Não encontrámos nenhumas histórias do Twine no ficheiro que carregaste. Tenta, por favor, um outro ficheiro.",
+ "selectAll": "Marcar tudo",
+ "title": "Importar histórias",
+ "uploadPrompt": "Para importar histórias para o Twine, carrega, em baixo, um ficheiro de arquivo de histórias ou um ficheiro de uma história.",
+ "willReplaceExisting": "A história da tua biblioteca com esse mesmo nome será substituída."
+ },
+ "storyList": {
+ "noStories": "Neste momento, não há histórias gravadas no Twine. Para começares, cria uma história nova ou importa uma de um ficheiro.",
+ "taggedTitleCount": "1 história etiquetada",
+ "taggedTitleCount_0": "Sem histórias etiquetadas",
+ "taggedTitleCount_plural": "{{count}} histórias etiquetadas",
+ "titleCount": "1 história",
+ "titleCount_0": "Sem histórias",
+ "titleCount_plural": "{{count}} Histórias",
+ "titleGeneric": "Histórias",
+ "topBar": {
+ "about": "Sobre o Twine",
+ "archive": "Arquivo",
+ "createStory": "História",
+ "help": "Ajuda",
+ "reportBug": "Reportar um erro",
+ "showAllStories": "Todas as histórias",
+ "showTags": "Mostrar etiquetas...",
+ "sort": "Ordenar por...",
+ "sortDate": "Data",
+ "sortName": "Nome",
+ "storyFormats": "Formatos",
+ "storyTags": "Editar as etiquetas da história"
+ }
+ },
+ "welcome": {
+ "autosave": "
Agora já tens uma pasta chamada Twine na tua pasta de Documentos. Dentro da pasta Twine, criámos uma pasta chamada Histórias, onde todos os teus trabalhos serão gravados. O Twine grava automaticamente enquanto trabalhas, portanto não precisas de te preocupar em gravar a tua história. Podes sempre abrir a pasta em que as tuas histórias estão gravadas através da opção Mostrar biblioteca no menu do Twine.
Como o Twine está sempre a gravar o teu trabalho, os ficheiros da tua biblioteca de histórias não podem ser editados enquanto o Twine estiver aberto.
Se quiseres abrir o ficheiro de uma história do Twine que recebeste de uma outra pessoa, podes importá-lo para a tua biblioteca usando a opção Importar ficheiro na lista de histórias.
",
+ "autosaveTitle": "O teu trabalho é gravado automaticamente.",
+ "browserStorage": "
Isto significa que não precisas de criar uma conta para usar o Twine 2, e tudo o que criares não fica armazenado num servidor sabe-se lá onde — fica aqui no teu navegador.
No entanto, há duas coisas muito importantes de que tens de te lembrar. Como o teu trabalho fica apenas gravado no teu navegador, se limpares os dados do navegador, vais perder o teu trabalho! O que não é nada bom. Lembra-te de usar o botão Arquivar com frequência. Também podes publicar cada história separadamente num ficheiro, usando a opção disponível em cada história, na lista de histórias. Tanto os ficheiros de arquivo, como os ficheiros individuais de história podem ser reimportados, a qualquer momento, para o Twine.
Em segundo lugar, qualquer pessoa que usar este navegador, pode ver e fazer alterações ao teu trabalho. Portanto, se tiveres um irmão mais novo abelhudo, talvez seja boa ideia criares um perfil separado só para ti.
",
+ "browserStorageTitle": "O teu trabalho fica apenas gravado no navegador",
+ "done": "
Obrigado por leres, e diverte-te com o Twine.
",
+ "doneTitle": "E é isto!",
+ "gotoStoryList": "Ir para a lista de histórias",
+ "greeting": "
O Twine é uma ferramenta de código-fonte aberto para contar histórias interativas e não lineares. Há algumas coisas que é importante saberes antes de começar.
",
+ "greetingTitle": "Olá!",
+ "help": "
Se nunca usaste o Twine antes, bem-vinda(o)! O O Livro de Receitas do Twine é um ótimo recurso para aprenderes a usar o programa. Se nunca usaste o Twine, é um ótimo lugar para começar.
",
+ "helpTitle": "É a primeira vez aqui?",
+ "tellMeMore": "Quero saber mais"
+ }
+ },
+ "store": {
+ "archiveFilename": "{{timestamp}} Arquivo_do_Twine.html",
+ "errors": {
+ "cantPersistPrefs": "Erro ao gravar as preferências ( {{error}} ).",
+ "cantPersistStories": "Erro ao gravar as tuas histórias ( {{error}} ).",
+ "cantPersistStoryFormats": "Erro ao gravar os formatos de história ( {{error}} ).",
+ "electronRemediation": "Reiniciar a aplicação poderá ajudar.",
+ "webRemediation": "Recarregar esta página poderá ajudar."
+ },
+ "passageDefaults": {
+ "name": "Passagem sem título",
+ "textClick": "Faz duplo-clique nesta passagem para editá-la.",
+ "textTouch": "Toca nesta passagem e, em seguida, no ícone do lápis para editá-la."
+ },
+ "storyDefaults": {
+ "name": "História sem título"
+ },
+ "storyFormatDefaults": {
+ "name": "Formato de história sem título"
+ }
+ },
+ "undoChange": {
+ "changeTagColor": "Mudar a cor da etiqueta",
+ "createPassage": "Criar passagem",
+ "deletePassage": "Apagar passagem",
+ "deletePassages": "Apagar passagens",
+ "movePassage": "Mover passagem",
+ "movePassages": "Mover passagens",
+ "removeTag": "Remover etiqueta",
+ "renamePassage": "Renomear passagem",
+ "renameTag": "Renomear etiqueta",
+ "replaceAllText": "Substituir tudo"
+ }
}
diff --git a/public/story-formats/chapbook-1.2.1/format.js b/public/story-formats/chapbook-1.2.1/format.js
index 022fb917d..6235e9d23 100644
--- a/public/story-formats/chapbook-1.2.1/format.js
+++ b/public/story-formats/chapbook-1.2.1/format.js
@@ -1 +1 @@
-window.storyFormat({"author":"Chris Klimas","description":"A Twine story format emphasizing ease of authoring, multimedia, and playability on many different types of devices. Visit the guide for more information.","image":"logo.svg","name":"Chapbook","proofing":false,"source":"{{STORY_NAME}}{{STORY_DATA}}","version":"1.2.1"});
\ No newline at end of file
+window.storyFormat({"author":"Chris Klimas","description":" This variant contains no testing-related code, and as a result, loads faster for players.","hydrate":"!function(e,t){for(var o in t)e[o]=t[o]}(this,function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var r in e)o.d(n,r,function(t){return e[t]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,\"a\",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p=\"\",o(o.s=127)}({127:function(e,t,o){\"use strict\";var n,r=o(128),l=(n=r)&&n.__esModule?n:{default:n};e.exports={editorExtensions:{twine:l.default}}},128:function(e,t,o){\"use strict\";Object.defineProperty(t,\"__esModule\",{value:!0});var n=u(o(129)),r=u(o(130)),l=u(o(131)),i=u(o(132));function u(e){return e&&e.__esModule?e:{default:e}}t.default={\"^2.4.0-alpha1\":{codeMirror:{commands:r.default,mode:n.default,toolbar:l.default},references:{parsePassageText:i.default}}}},129:function(e,t,o){\"use strict\";Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(){return{startState:function(){return{}},token:function(e,t){if(void 0===t.inVarsBlock){for(var o=1,n=e.lookAhead(o);n&&!t.inVarsBlock;)\"--\"===n&&(t.inVarsBlock=!0),n=e.lookAhead(++o);void 0===t.inVarsBlock&&(t.inVarsBlock=!1)}return t.inVarsBlock?e.sol()?e.match(/^--$/,!1)?(t.inVarsBlock=!1,e.skipToEnd(),\"punctuation\"):e.skipTo(\":\")?\"def\":(e.skipToEnd(),null):(e.skipToEnd(),null):e.sol()&&e.match(/^\\[.+\\]$/)?(e.skipToEnd(),\"keyword\"):e.match(/^\\[\\[[^\\]]+\\]\\]/)?\"link\":e.match(/^{[^}]+}/)?\"keyword\":(e.eatWhile(/[^[{]/)||e.skipToEnd(),null)}}}},130:function(e,t,o){\"use strict\";Object.defineProperty(t,\"__esModule\",{value:!0}),t.default={chapbookTest:function(e){console.log(\"Chapbook command!\",e)}}},131:function(e,t,o){\"use strict\";Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e,t){return console.log(\"CodeMirror toolbar\"),console.log(\"editor\",e),console.log(\"environment\",t),[{type:\"button\",command:\"chapbookTest\",icon:\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-rocket' width='44' height='44' viewBox='0 0 24 24' stroke-width='1.5' stroke='\"+(\"dark\"===t.appTheme?\"hsl(0, 0%, 70%)\":\"hsl(0, 0%, 30%)\")+\"' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3' /%3E%3Cpath d='M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3' /%3E%3Ccircle cx='15' cy='9' r='1' /%3E%3C/svg%3E\",label:\"Chapbook Test 1\"}]}},132:function(e,t,o){\"use strict\";Object.defineProperty(t,\"__esModule\",{value:!0}),t.default=function(e){return[\"test reference\"]}}}));","image":"logo.svg","name":"Chapbook","proofing":false,"source":"{{STORY_NAME}}{{STORY_DATA}}","version":"1.3.0"})
\ No newline at end of file
diff --git a/src/components/control/code-area/codemirror-theme.css b/src/components/control/code-area/codemirror-theme.css
index aa9f2d4c2..0a0be1c9d 100644
--- a/src/components/control/code-area/codemirror-theme.css
+++ b/src/components/control/code-area/codemirror-theme.css
@@ -25,7 +25,7 @@ The goal here is to align CM's default theme with our color palette.
font-style: italic;
}
.cm-link {
- text-decoration: underline;
+ text-decoration: none;
}
.cm-strikethrough {
text-decoration: line-through;
diff --git a/src/components/story/link-connectors/broken-connector.css b/src/components/passage/passage-connections/broken-connection.css
similarity index 86%
rename from src/components/story/link-connectors/broken-connector.css
rename to src/components/passage/passage-connections/broken-connection.css
index 0683b1327..46ca4bf81 100644
--- a/src/components/story/link-connectors/broken-connector.css
+++ b/src/components/passage/passage-connections/broken-connection.css
@@ -1,6 +1,6 @@
@import '../../../styles/colors.css';
-line.broken-connector {
+line.broken-connection {
/* See note in markers.tsx about how to apply arrowheads. */
fill: none;
stroke: var(--dark-red);
diff --git a/src/components/story/link-connectors/broken-connector.tsx b/src/components/passage/passage-connections/broken-connection.tsx
similarity index 78%
rename from src/components/story/link-connectors/broken-connector.tsx
rename to src/components/passage/passage-connections/broken-connection.tsx
index 0d96b8c39..ae88c344a 100644
--- a/src/components/story/link-connectors/broken-connector.tsx
+++ b/src/components/passage/passage-connections/broken-connection.tsx
@@ -1,9 +1,9 @@
import * as React from 'react';
import {Passage} from '../../../store/stories';
import {Point} from '../../../util/geometry';
-import './broken-connector.css';
+import './broken-connection.css';
-export interface BrokenConnectorProps {
+export interface BrokenConnectionProps {
offset: Point;
passage: Passage;
}
@@ -11,7 +11,7 @@ export interface BrokenConnectorProps {
// Making this equal in length to .
const lineOffset = 25 * Math.sqrt(2);
-export const BrokenConnector: React.FC = props => {
+export const BrokenConnection: React.FC = props => {
const {offset, passage} = props;
const start: Point = {left: passage.left, top: passage.top};
@@ -22,7 +22,7 @@ export const BrokenConnector: React.FC = props => {
return (
;
+ connections: Map>;
+ offset: Point;
+ self: Set;
+ variant?: 'link' | 'reference';
+}
+
+export const PassageConnectionGroup: React.FC = React.memo(
+ props => {
+ const {broken, connections, offset, self, variant = 'link'} = props;
+
+ return (
+ <>
+ {Array.from(connections).map(connection =>
+ Array.from(connection[1]).map(end => (
+
+ ))
+ )}
+ {Array.from(self).map(passage => (
+
+ ))}
+ {Array.from(broken).map(passage => (
+
+ ))}
+ >
+ );
+ }
+);
diff --git a/src/components/story/link-connectors/self-connector.css b/src/components/passage/passage-connections/passage-connection.css
similarity index 61%
rename from src/components/story/link-connectors/self-connector.css
rename to src/components/passage/passage-connections/passage-connection.css
index 3dae7551a..acc489bea 100644
--- a/src/components/story/link-connectors/self-connector.css
+++ b/src/components/passage/passage-connections/passage-connection.css
@@ -1,8 +1,12 @@
@import '../../../styles/colors.css';
-path.self-connector {
+path.passage-connection {
/* See note in markers.tsx about how to apply arrowheads. */
fill: none;
stroke: var(--gray);
stroke-width: 2px;
}
+
+path.passage-connection.variant-reference {
+ stroke-dasharray: 4px;
+}
diff --git a/src/components/story/link-connectors/passage-connector.tsx b/src/components/passage/passage-connections/passage-connection.tsx
similarity index 83%
rename from src/components/story/link-connectors/passage-connector.tsx
rename to src/components/passage/passage-connections/passage-connection.tsx
index 85e3b955e..060437429 100644
--- a/src/components/story/link-connectors/passage-connector.tsx
+++ b/src/components/passage/passage-connections/passage-connection.tsx
@@ -8,16 +8,17 @@ import {
Point
} from '../../../util/geometry';
import {Passage} from '../../../store/stories';
-import './passage-connector.css';
+import './passage-connection.css';
-export interface PassageConnectorProps {
+export interface PassageConnectionProps {
end: Passage;
offset: Point;
start: Passage;
+ variant: 'link' | 'reference';
}
-export const PassageConnector: React.FC = props => {
- const {end, offset, start} = props;
+export const PassageConnection: React.FC = props => {
+ const {end, offset, start, variant} = props;
const path = React.useMemo(() => {
// If either passage is selected, offset it. We need to take care not to
// overwrite the passage information.
@@ -48,11 +49,7 @@ export const PassageConnector: React.FC = props => {
// Move both points to where they intersect with the edges of their passages.
- startPoint = rectIntersectionWithLine(
- offsetStart,
- startPoint,
- endPoint
- );
+ startPoint = rectIntersectionWithLine(offsetStart, startPoint, endPoint);
if (!startPoint) {
return '';
@@ -79,10 +76,7 @@ export const PassageConnector: React.FC = props => {
let sweep = startPoint.left < endPoint.left;
- if (
- startPoint.left === endPoint.left &&
- startPoint.top < endPoint.top
- ) {
+ if (startPoint.left === endPoint.left && startPoint.top < endPoint.top) {
sweep = true;
}
@@ -103,7 +97,7 @@ export const PassageConnector: React.FC = props => {
return (
);
diff --git a/src/components/passage/passage-connections/passage-connections.tsx b/src/components/passage/passage-connections/passage-connections.tsx
new file mode 100644
index 000000000..c1260b9f7
--- /dev/null
+++ b/src/components/passage/passage-connections/passage-connections.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react';
+import {Passage, passageConnections} from '../../../store/stories';
+import {Point} from '../../../util/geometry';
+import {PassageConnectionGroup} from './passage-connection-group';
+import {LinkMarkers} from './markers';
+import {StartConnection} from './start-connection';
+import {useFormatReferenceParser} from '../../../store/use-format-reference-parser';
+
+export interface PassageConnectionsProps {
+ formatName: string;
+ formatVersion: string;
+ offset: Point;
+ passages: Passage[];
+ startPassageId: string;
+}
+
+const emptySet = new Set();
+const noOffset: Point = {left: 0, top: 0};
+
+export const PassageConnections: React.FC = props => {
+ const {formatName, formatVersion, offset, passages, startPassageId} = props;
+ const referenceParser = useFormatReferenceParser(formatName, formatVersion);
+ const {draggable: draggableLinks, fixed: fixedLinks} = React.useMemo(
+ () => passageConnections(passages),
+ [passages]
+ );
+ const {
+ draggable: draggableReferences,
+ fixed: fixedReferences
+ } = React.useMemo(() => passageConnections(passages, referenceParser), [
+ passages,
+ referenceParser
+ ]);
+
+ console.log(draggableReferences, fixedReferences);
+
+ const startPassage = React.useMemo(
+ () => passages.find(passage => passage.id === startPassageId),
+ [passages, startPassageId]
+ );
+
+ // References only show existing connections.
+
+ return (
+
+ );
+};
diff --git a/src/components/story/link-connectors/passage-connector.css b/src/components/passage/passage-connections/self-connection.css
similarity index 63%
rename from src/components/story/link-connectors/passage-connector.css
rename to src/components/passage/passage-connections/self-connection.css
index 3d75fcd5a..860a260b6 100644
--- a/src/components/story/link-connectors/passage-connector.css
+++ b/src/components/passage/passage-connections/self-connection.css
@@ -1,8 +1,12 @@
@import '../../../styles/colors.css';
-path.passage-connector {
+path.self-connection {
/* See note in markers.tsx about how to apply arrowheads. */
fill: none;
stroke: var(--gray);
stroke-width: 2px;
}
+
+path.self-connection.variant-reference {
+ stroke-dasharray: 4px;
+}
diff --git a/src/components/story/link-connectors/self-connector.tsx b/src/components/passage/passage-connections/self-connection.tsx
similarity index 77%
rename from src/components/story/link-connectors/self-connector.tsx
rename to src/components/passage/passage-connections/self-connection.tsx
index b2ed92905..82fc1f68b 100644
--- a/src/components/story/link-connectors/self-connector.tsx
+++ b/src/components/passage/passage-connections/self-connection.tsx
@@ -2,15 +2,16 @@ import * as React from 'react';
import {Passage} from '../../../store/stories';
import {Point} from '../../../util/geometry';
import {arc} from '../../../util/svg';
-import './self-connector.css';
+import './self-connection.css';
-export interface SelfConnectorProps {
+export interface SelfConnectionProps {
offset: Point;
passage: Passage;
+ variant: 'link' | 'reference';
}
-export const SelfConnector: React.FC = props => {
- const {offset, passage} = props;
+export const SelfConnection: React.FC = props => {
+ const {offset, passage, variant} = props;
const start: Point = {left: passage.left, top: passage.top};
if (passage.selected) {
@@ -46,7 +47,7 @@ export const SelfConnector: React.FC = props => {
return (
diff --git a/src/components/story/link-connectors/start-connector.css b/src/components/passage/passage-connections/start-connection.css
similarity index 72%
rename from src/components/story/link-connectors/start-connector.css
rename to src/components/passage/passage-connections/start-connection.css
index d3ef03ecd..2eaa51d4d 100644
--- a/src/components/story/link-connectors/start-connector.css
+++ b/src/components/passage/passage-connections/start-connection.css
@@ -1,4 +1,4 @@
-line.start-connector {
+line.start-connection {
fill: none;
stroke: var(--dark-green);
stroke-width: 2px;
diff --git a/src/components/story/link-connectors/start-connector.tsx b/src/components/passage/passage-connections/start-connection.tsx
similarity index 75%
rename from src/components/story/link-connectors/start-connector.tsx
rename to src/components/passage/passage-connections/start-connection.tsx
index f44a7ae9f..3955491e5 100644
--- a/src/components/story/link-connectors/start-connector.tsx
+++ b/src/components/passage/passage-connections/start-connection.tsx
@@ -1,14 +1,14 @@
import * as React from 'react';
import {Passage} from '../../../store/stories';
import {Point} from '../../../util/geometry';
-import './start-connector.css';
+import './start-connection.css';
-export interface StartConnectorProps {
+export interface StartConnectionProps {
offset: Point;
passage: Passage;
}
-export const StartConnector: React.FC = React.memo(
+export const StartConnection: React.FC = React.memo(
props => {
const {offset, passage} = props;
const start: Point = {left: passage.left, top: passage.top};
@@ -20,7 +20,7 @@ export const StartConnector: React.FC = React.memo(
return (
void;
onDrag: (change: Point) => void;
onEdit: (passage: Passage) => void;
@@ -67,6 +69,8 @@ const extraSpace = 500;
export const PassageMap: React.FC = props => {
const {
+ formatName,
+ formatVersion,
onDeselect,
onDrag,
onEdit,
@@ -141,7 +145,9 @@ export const PassageMap: React.FC = props => {
return (