Skip to content

dwmkerr/crosswords-js

Repository files navigation

crosswords-js

All Contributors

Release Please NPM Package Version codecov

IMPORTANT: This is work in progress! The API may change dramatically as I work out what is most suitable.

Tiny, lightweight crossword control for the web. crosswords-js is:

  • Lightweight
  • Fast
  • Simple
  • Framework-free

Inspired by the excellent free online crosswords on The Guardian Crosswords.

Demo: dwmkerr.github.io/crosswords-js/

CrosswordsJS Screenshot

Index

Documentation

The project documentation is written in Markdown and is located in the repository at ./docs.

Quickstart

  1. Install the package:

    npm install crosswords-js
  2. Include the minified JavaScript package source and CSS in your webpage:

    <link
      href="node_modules/crosswords-js/dist/crosswords.css"
      rel="stylesheet"
    />
    <script src="node_modules/crosswords-js/dist/crosswords.js"></script>
  3. To create a crossword, locate or edit a CrosswordSource, which can be imported from a simple JSON file to create a CrosswordDefinition:

    {
      "width": 15,
      "height": 15,
      "acrossClues": [
        {
          "x": 1,
          "y": 1,
          "clue": "1. Conspicuous influence exerted by active troops (8,5)"
        },
    
    ...
    
      ],
      "downClues": [
        {
          "x": 3,
          "y": 1,
          "clue": "1. A coy sort of miss pointlessly promoting lawlessness (9)"
        },
    
    ...
    
      ]
    }

    Complete CrosswordSource file examples can be found here, there or everywhere.

    Further on, the CrosswordDefinition needs to be compiled into a CrosswordModel. Compiling validates the the CrosswordDefinition, making sure that there are no incongruities in the structure, for example:

    • overlapping clues
    • clues which don't fit in the grid bounds
    • ...and so on.
  4. In your JavaScript code, load the crosswords-js package and a CrosswordDefinition:

    import { compileCrossword, newCrosswordController } from 'crosswords-js';
    import crosswordDefinition from 'node_modules/crosswords-js/data/ftimes_17095.json';
  5. Now get the DOM elements which will be the parents for the crossword grid and clues blocks:

    For example, if we have placeholder div elements somewhere in our webpage:

    ...
    <div id="crossword-grid-placeholder" />
    ...
    <div id="crossword-clues-placeholder" />

    We locate the element via the webpage DOM:

    const gridParent = document.getElementById('crossword-grid-placeholder');
    const cluesParent = document.getElementById('crossword-clues-placeholder');
  6. And pass the crosswordDefinition, gridParent and viewParent elements into the Controller constructor:

    let controller = newCrosswordController(
      crosswordDefinition,
      gridParent,
      cluesParent,
    );

    This binds the crossword gridView anf cluesView into the webpage DOM.

Application Programming Interface (API)

You can use the controller to programmatically manipulate the gridView - the crossword grid DOM element.

  1. Invoke the user event handlers
  • Call the user event handler methods of the controller directly in code

    // Check the current clue answer against the solution.
    controller.testCurrentClue();
  • Bind the user event handler methods via id or class attributes on DOM elements in your HTML markup, such as buttons.

    <div id="clue-buttons">
      <p>Clue</p>
      <button id="test-clue">Test</button>
      <button id="clean-clue">Clean</button>
      <button id="reveal-clue">Reveal</button>
      <button class="reset-clue">Reset</button>
      <button class="reset-clue">MoSet</button>
    </div>
    // Bind one element with id "test-clue"
    controller.bindEventHandlerToId("test-clue", "click", document);
    
    // Using default arguments for
    // eventName ("click") and dom (document)
    controller.bindEventHandlerToId("reveal-clue");
    
    // Bind event handler to multiple elements with class "reset-clue"
    // default arguments are available as before
    controller.bindEventHandlerToClass("reset-clue", "click", document);
    });
    
    // Bind ALL the user event handlers, using defaults
    controller.bindEventHandlersToIds();
    
    // Bind the user event handlers to ALL elements with
    // the given class(es), passing an array of one or more class names
    controller.bindEventHandlersToClass(["reset-clue"]);
  1. You can also provide your own handlers to listen to controller events.

For further information on these topics, consult the module API documentation.

For examples, refer to the development server code.

Styling

The library ships with some simple default styles out of the box, but aims to be easily customisable. See crossword-styling.md for details.

Sample applications

The development server is a pure Node.js application of the the crosswords-js package. It exercises nearly all the available functionality. The code is found in the dev directory of this repository.

# Open the development server on http://localhost:5173
npm start

Contributor guide

Setting up your dev environment

We strongly recommend you follow the popular "triangular" workflow, as recommended by GitHub, when working on this project. It aids collaboration by:

  • producing simple, linear commit sequences for pull-requests, and
  • easily incorporating changes in the upstream repo.

1a. Linux, MacOS

Check out the code and open the repository root directory...

git clone https://github.com/dwmkerr/crosswords-js.git &&
cd crosswords-js

then...

# From the repository root, bootstrap the package and all tools
bin/bootstrap-posix-ish.sh
# Open the development server
npm start

1b. Windows

If you are running a modern version of Windows, you can add a Linux distro to your computer using WSL and then follow the Linux instructions above.

1c. Manual setup

If the script above failed or doesn't suit your environment...

  1. Ensure you are using Node LTS. We recommend using Node Version Manager to make it easier to keep up to date:
# Install/update node to latest long-term-support (LTS) version, and install/update npm to latest version.
nvm install --lts --latest-npm
nvm use --lts
  1. Check out the code...
git clone https://github.com/dwmkerr/crosswords-js.git
  1. Open the repository root directory, install the packages, and start the development server...
cd crosswords-js
# Fetch all dependent packages
npm install
# Start the development server
npm start

2. Once you've started the development server

  • The development server webpage will be visible at http://localhost:5173/
    • The webpage will dynamically refresh whenever you save your source edits
  • View the development webpage HTML: dev/index.html
  • View the development webpage JavaScript: dev/index.js
  • View the development webpage styles via the less source: dev/index.less
  • Less files are dynamically compiled to CSS by ViteJS for the development server.

Maintaining your dev environment

If you have installed Node Version Manager (nvm) following the recommended procedure, you can keep up with the latest versions of nvm, npm, node LTS, and the latest package versions for this module by regularly running:

# Update the tools and packages used in this environment
npm run update

Quality assurance

You can automate the manual checks in the section below on each commit to your local git repository.

npm run qa:install

If you ever need to bypass the automated checks, stage your changes then run:

git commit --no-verify

Manual checks

  1. We use MochaJS for unit testing. The test source code is located in the repository at ./test. Run the tests with:

    npm test
  2. Linting is provided by ESLint, which is also configured to use Prettier for code formatting:

    # Lint the code.
    npm run lint
    # Lint and fix the code.
    npm run lint:fix
  3. Documentation and HTML can be checked for standard conformance using Prettier:

    # Check html and docs for correctness.
    npm run prettier
    # Check and fix html and docs for correctness.
    npm run prettier:fix
  4. Spelling can be checked using CSpell:

    # Check _staged_ files for spelling.
    npm run spell
    # Check new and changed files for spelling.
    npm run spell:changed
    # Check all files for spelling.
    npm run spell:all
  5. Ensure you build and stage the production assets

    # Build and stage the production assets
    npm run build && git add dist/
  6. Please install our git commit template. This enables project commit guidelines to be prefixed to the standard git commit message. From the root directory of your repository:

    git config --local commit.template ./.git-commit-template.txt

Building the dev environment assets for production

The dev environment production assets are built by ViteJS at dev/dist. The dist folder is created when the assets are built.

# Build the assets under dev/dist
npm run dev:build

You can preview the production assets by running the following command and opening a browser on http://localhost:4173/

# Build the assets and preview locally at http://locahost:4173
npm run dev:preview

Keyboard functionality

You can also find these keyboard shortcuts in the documentation

These are the default shortcuts:

  • Left/Right/Up/Down: Move (if possible) to the cell in the direction specified.
  • Space: Move to the next cell in the focused clue, if one exists.
  • Shift+Space: Move to the previous cell in the focused clue, if one exists.
  • Delete: Delete the current cell.
  • Backspace: Delete the current cell, and move to the previous cell in the focused clue, if one exists.
  • Tab: Move to the first cell of the next clue, 'wrapping' to the first clue in the opposite direction.
  • Shift+Tab: Move to the last cell of the previous clue, 'wrapping' to the last clue in the opposite direction.
  • A-Z: Enter the character. Not locale aware!
  • Enter: At a clue intersection, switch between across and down.

You can override the default shortcuts by create your own eventBinding sets. This is described in an API use case.

Crossword definition tips

1. How do I create a clue which spans multiple parts of a crossword? (multi-segment clue)

This is a little fiddly. I have tried to ensure the syntax is as close to what a reader would see in a printed crossword to make this as clear as possible. Here is an example:

{
  "downClues": [{
    "x": 6, "y": 1
    "clue": "4,21. The king of 7, this general axed threat strategically (9)"
  }],
  "acrossClues": [{
    "x": 1, "y": 11,
    "clue": "21. See 4 (3,5)"
  }]
}

Note that the LengthText (which would be (9,3,5) in a linear clue) has separated. However, the crossword GridView will render the full LengthText for the first (head) clue segment (and nothing for the tail segments).

An example of a crossword with many multi-segment clues is at: https://www.theguardian.com/crosswords/cryptic/28038 - I have used this crossword for testing (but not included the definition in the codebase as I don't have permissions to distribute it).

2. How do I style the clue content with bold, italic and bold-italic words?

We support a subset of Markdown.

  • Style a word or phrase by book-ending the text with matching tags described below, for example: **bold** text. These Markdown tags are converted to CSS styles in the cluesView, or anywhere else clues are displayed.
  • You can style just a part of a word by embedding the tags within the word, for example: partial*italic*s
  • You can even embed a style within a style, for example: a _comp**lic**ated_ example
Style Markdown tag Example Associated CSS class
italic _ or * Some _italic_ text. .cw-italic { font-style: italic; }
bold __ or ** Some **bold** text. .cw-bold { font-weight: bold; }
bold-italic ___ or *** Some ___bold, italic___ text. The classes above are combined.

3. How do I handle different grid sizes?

We determine the GridView dimensions dynamically whenever a CrosswordSource is loaded.

Design overview

The design of this project follows the Model-view-controller (MVC) design pattern. The naming of files and code artifacts follow from this pattern.

Design goals

This project is currently a work in progress. The overall design goals are:

  1. This should be agnostic to the type of crossword. It shouldn't depend on any proprietary formats or structures used by specific publications.
  2. This should be accessible, and show how to make interactive content which is inclusive and supports modern accessibility patterns.
  3. This project should be simple to use, without requiring a lot of third party dependencies or knowledge.

Build workflows

There are two workflows that run for the project:

Pull-request workflow

Whenever a pull request is raised, the Pull Request Workflow is run. This will:

  • Install dependencies
  • Lint
  • Run Tests
  • Upload Coverage

Each stage is run on all recent Node versions, except for the upload coverage stage which only runs for the Node.js LTS version. When a pull request is merged to the main branch, if the changes trigger a new release, then Release Please will open a Release Pull Request. When this request is merged, the Main Workflow is run.

Main workflow

When a Release Please pull request is merged to main, the Main Workflow is run. This will:

  • Install dependencies
  • Lint
  • Run Tests
  • Upload Coverage
  • Deploy to NPM if the NPM_TOKEN secret is set
  • Upload the new release to the GitHub Pages site

Each stage is run on all recent Node versions, except for the upload coverage stage which only runs for the Node.js LTS version.

⚠️ Note that the NPM Publish step sets the package to public - don't forget to change this if you have a private module.

Adding contributors

To add contributors, use a comment like the below in anNode.jsy pull request:

@all-contributors please add @<username> for docs, code, tests

More detailed documentation is available at:

allcontributors.org/docs/en/bot/usage

Managing releases

When changes to main are made, the Release Please stage of the workflow will work out whether a new release should be generated (by checking if there are user facing changes) and also what the new version number should be (by checking the log of conventional commits). Once this is done, if a release is required, a new pull request is opened that will create the release.

Force a specific release version with this command:

# Specify your version. We use Semantic Versioning (https://semver.org/)
version="0.1.0"
git commit --allow-empty -m "chore: release ${version}" -m "Release-As: ${version}"

Contributors

Dave Kerr
Dave Kerr

πŸ“– πŸ’» ⚠️
Paul Spain
Paul Spain

πŸ“– πŸ’» ⚠️
Misha Kaletsky
Misha Kaletsky

πŸ’» πŸ“– πŸ‘€

TODO

This is a scattergun list of things to work on, once a good chunk of these have been done the larger bits can be moved to GitHub Issues:

  • bug: backspace moves backwards, I think that deleting the letter is a better action for this (with left/up/ key to move backwards)
  • bug: Demo site is not tracking latest version
  • bug: Demo site seems to have loading issues
  • feat(docs): improve the demo site image (its an old one at the moment!)
  • feat: show how we can check answers or highlight incorrect entries (see issue #9)
  • feat(samples): allow us to switch between 2-3 crosswords on the sample
  • feat(samples): cursor initially on the first clue
  • feat(dom): support a keyboard scheme or configurable keybindings so that keys for navigating / editing the crossword can be specified in config (allowing for schemes such as 'the guardian' or 'the age')
  • fix: the border on word separators slightly offsets the rendering of the grid
  • fix: the border on word separators in 'down' clues. Only partially extends across cell-width. (See "14 down" clue in "Financial Times 17,095" test crossword)
  • feat(accessibility): get screenreader requirements
  • refactor: Simplify the static site by removing Angular and Bootstrap, keeping everything as lean and clean as possible. Later, replace with a React sample? OR have multiple samples, one for each common framework?
  • refactor: finish refactoring
  • feat: support clues which span non-contiguous ranges (such as large clues with go both across and down).
  • feat: simplify the crossword model by using a or d for across or down in the clue text (meaning we don't have to have two arrays of clues)
  • feat: allow italics with underscores, or bold with stars (i.e. very basic markdown)...
  • feat: clicking the first letter of a clue which is part of another clue should allow for a toggle between directions
  • todo: document the clue structure
  • refactor: re-theme site to a clean black and white serif style, more like a newspaper
  • build: enforce linting (current it is allowed to fail)