{"id":16222917,"url":"https://github.com/michael/substance","last_synced_at":"2025-04-08T01:41:49.201Z","repository":{"id":36798040,"uuid":"41104854","full_name":"michael/substance","owner":"michael","description":"Copy of Substance for legacy purposes.","archived":false,"fork":false,"pushed_at":"2015-08-30T18:34:28.000Z","size":4033,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-02-14T01:46:00.988Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michael.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-08-20T15:52:54.000Z","updated_at":"2019-08-18T16:50:13.000Z","dependencies_parsed_at":"2022-09-04T04:20:13.890Z","dependency_job_id":null,"html_url":"https://github.com/michael/substance","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael%2Fsubstance","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael%2Fsubstance/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael%2Fsubstance/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael%2Fsubstance/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michael","download_url":"https://codeload.github.com/michael/substance/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247761052,"owners_count":20991533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T12:15:49.637Z","updated_at":"2025-04-08T01:41:49.169Z","avatar_url":"https://github.com/michael.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Substance\n\nSubstance is a Javascript library for developing rich editing experiences. From simple text editors to full-featured publishing systems: Substance provides you reliable building blocks to realize your project.\n\n**Custom document models**: Define a custom article schema, by including common content types, such as paragraphs and headings and defining custom elements.\n\n**Operation based manipulation**: Substance documents are manipulated with operations that can be undone, redone and distributed over a network for concurrent manipulations (collaborative editing)\n\n**HTML/XML import export:** Substance interacts well with HTML/XML content. E.g. you can import whole XML documents or insert HTML fragments from the clipboard.\n\n**Server and client-side execution**: Substance runs in the browser and server-side environments, such as Node.js or io.js.\n\nSee Substance in action:\n\n- **[Substance HTML Editor](http://cdn.substance.io/html-editor)** - A minimal HTML editor component based on Substance\n- **[Lens Writer](http://cdn.substance.io/lens-writer)** - A full-fledged scientific editor\n- **[Archivist Writer](http://cdn.substance.io/archivist-composer)** - A modern interface for tagging entities and subjects in digital archives\n\n## Motivation\n\nBuilding a web editor is a hard task. Native browser support for text editing is [limited and not reliable](https://medium.com/medium-eng/why-contenteditable-is-terrible-122d8a40e480) and there are many pitfalls such as handling selections, copy\u0026paste or undo/redo. Substance was developed to solve the common problems of web-editing and provides API's for building custom editors.\n\nWith Substance you can:\n\n- Define a *custom article schema*\n- Manipulate content and annotations using *operations* and *transactions*\n- Define a custom HTML structure and attach a `Substance Surface` on it to make it editable\n- Implement custom tools for any possible task like toggling annotations, inserting content or replacing text\n- Control *undo/redo* behavior and *copy\u0026paste*\n- and more.\n\n## Getting started\n\nA good way to get started with Substance is checking out our fully customizable and highly reliable HTML Editor component. Unlike most web-based editors, Substance operates on a Javascript data model, and uses HTML just as an input and output format. You can inject the editor using `React.render`.\n\n```js\nvar HtmlEditor = require('substance-html-editor');\n\nReact.render(\n  $$(HtmlEditor, {\n    ref: 'htmlEditor',\n    content: '\u003cp\u003eHello \u003cstrong\u003eworld\u003c/strong\u003e\u003c/p\u003e\u003cp\u003eSome \u003cem\u003eemphasized\u003c/em\u003e text\u003c/p\u003e',\n    toolbar: Toolbar,\n    enabledTools: [\"text\", \"strong\", \"emphasis\"],\n    onContentChanged: function(doc, change) {\n      console.log('new content', doc.toHtml());\n    }\n  }),\n  document.getElementById('editor_container')\n);\n```\n\nPlease see the [README](https://github.com/substance/html-editor) of HtmlEditor for configuration options.\n\n## Defining custom article formats.\n\nSubstance allows you to define completely custom article formats. \n\n```js\nvar Paragraph = Substance.Document.Paragraph;\nvar Emphasis = Substance.Document.Emphasis;\nvar Strong = Substance.Document.Strong;\n\nvar Highlight = Document.ContainerAnnotation.extend({\n  name: 'highlight',\n  properties: {\n    created_at: 'date'\n  }\n});\n\nvar schema = new Document.Schema(\"my-article\", \"1.0.0\");\nschema.getDefaultTextType = function() {\n  return \"paragraph\";\n};\nschema.addNodes([Paragraph, Emphasis, Strong, Highlight]);\n```\n\nA very simple one is the [HtmlArticle specification](https://github.com/substance/html-editor/blob/master/src/html_article.js) used by our HtmlEditor. Lens Writer defines a [scientific article](https://github.com/substance/lens-writer/tree/master/lib/article) including bib items and figures with captions etc.\n\n\n## Manipulate documents programmatically\n\nSubstance documents can be manipulated incrementally using simple operations. Let's grab an existing article implementation and create instances for it.\n\n```js\nvar doc = new RichTextArticle();\n```\n\nWhen you want to update a document, you should wrap your changes in a transaction, so you don't end up in inconsistent in-between states. The API is fairly easy. Let's create several paragraph nodes in one transaction\n\n```js\ndoc.transaction(function(tx) {\n  tx.create({\n    id: \"p1\",\n    type: \"paragraph\",\n    content: \"Hi I am a Substance paragraph.\"\n  });\n\n  tx.create({\n    id: \"p2\",\n    type: \"paragraph\",\n    content: \"And I am the second pargraph\"\n  });\n});\n\n\n```\n\nA Substance document works like an object store, you can create as many nodes as you wish and assign unique id's to them. However in order to show up as content, we need to show them on a container.\n\n```js\ndoc.transaction(function(tx) {\n  // Get the body container\n  var body = tx.get('body');\n\n  body.show('p1');\n  body.show('p2');\n});\n```\n\nNow let's make a **strong** annotation. In Substance annotations are stored separately from the text. Annotations are just regular nodes in the document. They refer to a certain range (`startOffset, endOffset`) in a text property (`path`).\n\n```js\ndoc.transaction(function(tx) {\n  tx.create({\n    \"id\": \"s1\",\n    \"type\": \"strong\",\n    \"path\": [\n      \"p1\",\n      \"content\"\n    ],\n    \"startOffset\": 10,\n    \"endOffset\": 19\n  });\n});\n```\n\n## Developing editors\n\nIn order to build your own editor based on Substance we recommend that you poke into the code of existing editors. HtmlEditor implements an editor for HTML content in [under 200 lines of code](https://github.com/substance/html-editor/blob/master/src/html_editor.js).\n\n\n### Editor initialization\n\nEditors to setup a bit of Substance infrastructure first, most importantly a Substance Surface, that maps DOM selections to internal document selections. Here's the most important parts from the initialization phase.\n\n```js\nthis.surfaceManager = new Substance.Surface.SurfaceManager(doc);\nthis.clipboard = new Substance.Surface.Clipboard(this.surfaceManager, doc.getClipboardImporter(), doc.getClipboardExporter());\nvar editor = new Substance.Surface.ContainerEditor('body');\nthis.surface = new Surface(this.surfaceManager, doc, editor);\n```\n\nA Surface instance requires a `SurfaceManager`, which keeps track of multiple Surfaces and dispatches to the currently active one. It also requires an editor. There are two kinds of editors: A ContainerEditor manages a sequence of nodes, including breaking and merging of text nodes. A FormEditor by contrast allows you to define a fixed structure of your editable content. Furthermore we initialized a clipboard instance and tie it to the Surface Manager.\n\nWe also setup a registry for components (such as Paragraph) and tools (e.g. EmphasisTool, StrongTrool). Our editor will then be able to dynamically retrieve the right view component for a certain node type.\n\n### 2-column editing\n\nWe provide a framework, that allows building \n\n\u003c!-- ## Getting started\n\nLet's develop a basic Rich Text Editor using Substance. We will define a simple article format, and an editor to manipulate it in the browser. Follow our guide here to get a feeling about the available concepts. Get your hands dirty by playing around with our [starter package](https://github.com/substance/starter) and if you feel more ambitious you can look at our [Science Writer](https://github.com/substance/science-writer) app.\n\n### Define a custom article format\n\nModelling a schema is easy.\n\n```js\nvar schema = new Substance.Document.Schema(\"rich-text-article\", \"1.0.0\");\n```\n\nSubstance has a number of predefined commonly used Node types, that we are going to borrow for our schema. But defining our own is very simple too. We'll define a node type highlight, just as another annotation type. We choose to use a container annotation type, which means that the annotation can span over multiple paragraphs. Regular annotations (like our emphasis and strong) are scoped to one text property.\n\n```js\nvar Paragraph = Substance.Document.Paragraph;\nvar Emphasis = Substance.Document.Emphasis;\nvar Strong = Substance.Document.Strong;\n\nvar Highlight = Document.ContainerAnnotation.extend({\n  name: 'highlight',\n  properties: {\n    created_at: 'date'\n  }\n});\n\nschema.addNodes([\n  Paragraph,\n  Emphasis,\n  Strong,\n  Highlight\n]);\n```\n\nWe need to specify a default text type, which will be the node being created when you hit enter.\n\n```js\nschema.getDefaultTextType = function() {\n  return \"paragraph\";\n};\n```\n\nBased on Substance Document, we now define a Javascript class, that will hold our future documents.\n\n```js\nvar RichTextArticle = function() {\n  RichTextArticle.super.call(this, schema);\n};\n\nRichTextArticle.Prototype = function() {\n  this.initialize = function() {\n    this.super.initialize.apply(this, arguments);\n\n    // We will create a default container node `body` that references arbitrary many\n    // content nodes, most likely paragraphs.\n    this.create({\n      type: \"container\",\n      id: \"body\",\n      nodes: []\n    });\n  };\n};\n\nSubstance.inherit(RichTextArticle, Document);\n\nRichTextArticle.schema = schema;\n```\n\n### Create an article programmatically\n\nCreate a new document instance.\n\n```js\nvar doc = new RichTextArticle();\n```\n\nCreate several paragraph nodes\n\n```js\ndoc.create({\n  id: \"p1\",\n  type: \"paragraph\",\n  content: \"Hi I am a Substance paragraph.\"\n});\n\ndoc.create({\n  id: \"p2\",\n  type: \"paragraph\",\n  content: \"And I am the second pargraph\"\n});\n```\n\nA Substance document works like an object store, you can create as many nodes as you wish and assign unique id's to them. However in order to show up as content, we need to show them on a container.\n\n```js\n// Get the body container\nvar body = doc.get('body');\n\nbody.show('p1');\nbody.show('p2');\n```\n\nNow let's make a **strong** annotation. With Substance you store annotations separate from the text. Annotations are just regular nodes in the document. They refer to a certain range (startOffset, endOffset) in a text property (path).\n\n```js\ndoc.create({\n  \"id\": \"s1\",\n  \"type\": \"strong\",\n  \"path\": [\n    \"p1\",\n    \"content\"\n  ],\n  \"startOffset\": 10,\n  \"endOffset\": 19\n});\n```\n\nSo that's enough for the start. Now let's create an editor.\n\n### Build an editor\n\nSee: [editor.js](https://github.com/substance/starter/blob/master/src/editor.js)\n\nWe're using React for our example, but be aware that you can use Substance with any other web framework, or not use a framework at all.\n\nOur Editor component receives the document as an input. Now as a first step, our editor should be able to render the document passed in. We will set up a bit of Substance infrastructure first, most importantly a Substance Surface, that maps DOM selections to internal document selections.\n\n#### Editor initialization\n\n```js\nthis.surfaceManager = new Substance.Surface.SurfaceManager(doc);\nthis.clipboard = new Substance.Surface.Clipboard(this.surfaceManager, doc.getClipboardImporter(), doc.getClipboardExporter());\nvar editor = new Substance.Surface.ContainerEditor('body');\nthis.surface = new Surface(this.surfaceManager, doc, editor);\n```\n\nA Surface instance requires a `SurfaceManager`, which keeps track of multiple Surfaces and dispatches to the currently active one. It also requires an editor. There are two kinds of editors: A ContainerEditor manages a sequence of nodes, including breaking and merging of text nodes. A FormEditor by contrast allows you to define a fixed structure of your editable content. Furthermore we initialized a clipboard instance and tie it to the Surface Manager.\n\nWe also setup a registry for components (such as Paragraph) and tools (e.g. EmphasisTool, StrongTrool). Our editor will then be able to dynamically retrieve the right view component for a certain node type.\n\n\nWe also need to hook into `componentDidMount` to attach our Surface and clipboard to the corresponding DOM elements as soon as they get available.\n\n```js\ncomponentDidMount() {\n  var doc = this.props.doc;\n\n  doc.connect(this, {\n    'document:changed': this.onDocumentChanged\n  });\n\n  this.surfaceManager.registerSurface(this.surface, {\n    enabledTools: ENABLED_TOOLS\n  });\n\n  this.surface.attach(this.refs.bodyNodes.getDOMNode());\n\n  this.surface.connect(this, {\n    'selection:changed': this.onSelectionChanged\n  });\n\n  this.clipboard.attach(React.findDOMNode(this));\n\n\n  this.forceUpdate(function() {\n    this.surface.rerenderDomSelection();\n  }.bind(this));\n}\n```\n\nWe bind some event handlers:\n\n  - `onDocumentChange` to trigger an editor rerender if the container changes (a node is added or removed)\n  - `onSelectionChanged` to update the tools based on the new document selection\n\nWe'll look into those handler implementations later. First, let's render our document.\n\n#### Render the document\n\nFor each node type we defined we need to define a component class with a render method. Here's how our paragraph implementation looks like:\n\n\nSee: [components/paragraph.js](https://github.com/substance/starter/blob/master/src/components/paragraph.js)\n\n```js\nvar TextProperty = require('substance-ui/text_property');\nvar $$ = React.createElement;\n\nclass Paragraph extends React.Component {\n  render() {\n    return $$(\"div\", { className: \"content-node paragraph\", \"data-id\": this.props.node.id },\n      $$(TextProperty, {\n        doc: this.props.doc,\n        path: [ this.props.node.id, \"content\"]\n      })\n    );\n  }\n}\n```\n\nThe paragraph is represented as a simple div. However the text rendering is where things get difficult. Substance provides a generic implementation TextProperty for rendering annotated text. We just use this component here and refer to a path (paragraph id and property name).\n\nWe don't need to implement annotation nodes (strong, emphasis), as there is a default renderer implemented for annotations. Now that we have our components ready, we can head over to implementing the `render` method of our editor:\n\n\n```js\nrender() {\n  var doc = this.props.doc;\n  var containerNode = doc.get('body');\n  var components = [];\n\n  components = components.concat(containerNode.nodes.map(function(nodeId) {\n    var node = doc.get(nodeId);\n    var ComponentClass = this.componentRegistry.get(node.type);\n    return $$(ComponentClass, { key: node.id, doc: doc, node: node });\n  }.bind(this)));\n\n  return $$('div', {className: 'editor-component'},\n    $$('div', {className: 'toolbar'},\n      $$(ToolComponent, { tool: 'emphasis', title: 'Emphasis', classNames: ['button', 'tool']}, \"Emphasis\"),\n      $$(ToolComponent, { tool: 'strong', title: 'Strong', classNames: ['button', 'tool']}, \"Strong\")\n    ),\n    $$('div', {className: 'body-nodes', ref: 'bodyNodes', contentEditable: true, spellCheck: false},\n      components\n    )\n  );\n}\n```\n\nEssentially what we do is iterating over all nodes of our body container, determining the ComponentClass and constructing a React.Element from it. We also provided a simple toolbar, that has annotation toggles. We will learn more about tools later when we implement a custom tool for our editor.\n\n\n\n\n### Anatomy of a Substance Document\n\nTODO: describe\n\n- Nodes\n- Properties\n- Containers\n\n\n### Transactions\n\nWhen you want to update a document, you should wrap all your changes in a transaction, so you don't end up in inconsistent in-between states. The API is fairly easy:\n\n```js\ndoc.transaction(function(tx) {\n  tx.delete(\"em2\"); // deletes an emphasis annotation with id em2\n});\n```\n\n```js\nvar updated = \"Hello world!\";\ndoc.transaction(function(tx) {\n  tx.set([text_node_1, \"content\"], updated); // updates content property of node text_node_1\n});\n```\n\n## Rules that make your life easier:\n\n- Content tools must bind to mousedown instead of click to handle toggling.\n  That way we can prevent the blur event to be fired on the surface.\n- The root element of a Substance Surface must be set contenteditable\n\n--\u003e\n\n## Development\n\n### Testing\n\n1. Running the test-suite headless (using Phantom.js)\n\n```\n$ npm test\n```\n\n2. Running the test-suite in a browser for debugging:\n\n```\n$ npm start\n```\n\nThen open http://localhost:4201/test in your browser.\n\n3. Running test-suite using Karma to generate a code coverage report.\n\n```\n$ npm run karma\n```\n\nThe report will be stored in the `coverage` folder.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichael%2Fsubstance","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichael%2Fsubstance","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichael%2Fsubstance/lists"}