Mathjax: Is there a way to set `cjkCharWidth` in MathJax3 ?

Created on 22 Feb 2020  路  11Comments  路  Source: mathjax/MathJax

In MathJax-node, there is an option called cjkCharWidth to control the width of CJK character: https://github.com/mathjax/MathJax-node#typesetoptions-callback
However, in MathJax3 this option is no longer available. I read the documentation and source code but didn't get help. Thus I would like to ask if there is a way to achieve similar functionality in MathJax3? Thank you!

Accepted Feature Request Merged Needs Documentation Test Needed v3

Most helpful comment

There is now a PR for this.

All 11 comments

I'm assuming you are working in a node application on the server, as in the browser the width should be measure properly automatically.

Are you using MathJax components, or direct calls to the MathJax modules (see the MathJax node examples for illustrations of each)? The way you would handle this depends on which approach you are using.

Thank you for your reply. Yes, I am working in a node application on the server, and the program is based on this demo:

https://github.com/mathjax/MathJax-demos-node/blob/master/direct/tex2svg-page

OK, here is a modified version of tex2svg that allows you to specify the CJK width (and normal width as well).

#! /usr/bin/env -S node -r esm

/*************************************************************************
 *
 *  direct/tex2svg
 *
 *  Uses MathJax v3 to convert a TeX string to an SVG string.
 *
 * ----------------------------------------------------------------------
 *
 *  Copyright (c) 2018 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

//
//  Load the packages needed for MathJax
//
const mathjax = require('mathjax-full/js/mathjax.js').mathjax;
const TeX = require('mathjax-full/js/input/tex.js').TeX;
const SVG = require('mathjax-full/js/output/svg.js').SVG;
const LiteAdaptor = require('mathjax-full/js/adaptors/liteAdaptor.js').LiteAdaptor;
const RegisterHTMLHandler = require('mathjax-full/js/handlers/html.js').RegisterHTMLHandler;

const AllPackages = require('mathjax-full/js/input/tex/AllPackages.js').AllPackages;

//
//  Subclass the LiteAdaptor to modify how widths are determined
//
class myAdaptor extends LiteAdaptor {
  isCJK(c) {
    return (
      (c >= 0x3400 && c < 0x4DC0) ||
      (c >= 0x4E00 && c < 0xA000) ||
      (c >= 0xF900 && c < 0xFB00) ||
      (c >= 0x20000 && c < 0xE0000)
    );  
  }

  nodeSize(node, em = 1, local = null) {
    const cjk = this.options.cjkWidth;
    const width = this.options.normalWidth;
    const text = this.textContent(node);
    let w = 0;
    for (const c of text.split('')) {
      w += (this.isCJK(c.codePointAt(0)) ? cjk : width);
    }
    return [w / this.options.em, 0];
  }
}

myAdaptor.OPTIONS = {
  ...LiteAdaptor.OPTIONS,
  cjkWidth: 13,              // the default CJK width
  normalWidth: 8.5,          // the default normal (monospace) width
  em: 16                     // the default em size
};


//
//  Get the command-line arguments
//
var argv = require('yargs')
    .demand(0).strict()
    .usage('$0 [options] "math" > file.svg')
    .options({
        inline: {
            boolean: true,
            describe: "process as inline math"
        },
        em: {
            default: 16,
            describe: 'em-size in pixels'
        },
        ex: {
            default: 8,
            describe: 'ex-size in pixels'
        },
        width: {
            default: 80 * 16,
            describe: 'width of container in pixels'
        },
        cjkWidth: {
            default: 13,
            describe: 'width of CJK characters'
        },
        normalWidth: {
            default: 8.5,
            describe: 'width of unknown non-cjk characters'
        },
        packages: {
            default: AllPackages.sort().join(', '),
            describe: 'the packages to use, e.g. "base, ams"'
        },
        css: {
            boolean: true,
            describe: 'output the required CSS rather than the SVG itself'
        },
        fontCache: {
            boolean: true,
            default: true,
            describe: 'whether to use a local font cache or not'
        }
    })
    .argv;

//
//  Create DOM adaptor and register it for HTML documents
//
const adaptor = new myAdaptor({
    cjkWidth: argv.cjkWidth,
  normalWidth: argv.normalWidth,
  em: argv.em
});
RegisterHTMLHandler(adaptor);

//
//  Create input and output jax and a document using them on the content from the HTML file
//
const tex = new TeX({packages: argv.packages.split(/\s*,\s*/)});
const svg = new SVG({fontCache: (argv.fontCache ? 'local' : 'none')});
const html = mathjax.document('', {InputJax: tex, OutputJax: svg});

//
//  Typeset the math from the command line
//
const node = html.convert(argv._[0] || '', {
    display: !argv.inline,
    em: argv.em,
    ex: argv.ex,
    containerWidth: argv.width
});

//
//  If the --css option was specified, output the CSS,
//  Otherwise, typeset the math and output the HTML
//
if (argv.css) {
    console.log(adaptor.textContent(svg.styleSheet(html)));
} else {
    console.log(adaptor.outerHTML(node));
}

The main change is the addition of the myAdaptor class that implements the new measuring of unknown text. Note in particular the change in capitalization when loading LiteAdaptor rather than liteAdaptor in the require() statements at the top.

I also added command-line options to allow you to control the values without altering the program. These are passed to the new myAdaptor() call (that replaces liteAdaptor()) as options.

I think that should do what you want.

Thanks, the calculation of width is perfect now.
鎴睆2020-02-25涓嬪崍1 25 12

In addition, I found a npm package named string-width, which is used to determine the width of Unicode characters, because codePointAt does not always seem accurate in some languages. Hope this helps more people.

OK, here is a modified version of tex2svg that allows you to specify the CJK width (and normal width as well).

#! /usr/bin/env -S node -r esm

/*************************************************************************
 *
 *  direct/tex2svg
 *
 *  Uses MathJax v3 to convert a TeX string to an SVG string.
 *
 * ----------------------------------------------------------------------
 *
 *  Copyright (c) 2018 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

//
//  Load the packages needed for MathJax
//
const mathjax = require('mathjax-full/js/mathjax.js').mathjax;
const TeX = require('mathjax-full/js/input/tex.js').TeX;
const SVG = require('mathjax-full/js/output/svg.js').SVG;
const LiteAdaptor = require('mathjax-full/js/adaptors/liteAdaptor.js').LiteAdaptor;
const RegisterHTMLHandler = require('mathjax-full/js/handlers/html.js').RegisterHTMLHandler;

const AllPackages = require('mathjax-full/js/input/tex/AllPackages.js').AllPackages;

//
//  Subclass the LiteAdaptor to modify how widths are determined
//
class myAdaptor extends LiteAdaptor {
  nodeSize(node, em = 1, local = null) {
    const cjk = this.options.cjkWidth;
    const width = this.options.normalWidth;
    const text = this.textContent(node);
    let w = 0;
    for (const c of text.split('')) {
      w += (c.codePointAt(0) ? cjk : width);
    }
    return [w, 0];
  }
}

myAdaptor.OPTIONS = {
  ...LiteAdaptor.OPTIONS,
  cjkWidth: 13,              // the default CJK width
  normalWidth: 8.5           // the default normal (monospace) width
};


//
//  Get the command-line arguments
//
var argv = require('yargs')
    .demand(0).strict()
    .usage('$0 [options] "math" > file.svg')
    .options({
        inline: {
            boolean: true,
            describe: "process as inline math"
        },
        em: {
            default: 16,
            describe: 'em-size in pixels'
        },
        ex: {
            default: 8,
            describe: 'ex-size in pixels'
        },
        width: {
            default: 80 * 16,
            describe: 'width of container in pixels'
        },
        cjkWidth: {
            default: 13,
            describe: 'width of CJK characters'
        },
        normalWidth: {
            default: 8.5,
            describe: 'width of unknown non-cjk characters'
        },
        packages: {
            default: AllPackages.sort().join(', '),
            describe: 'the packages to use, e.g. "base, ams"'
        },
        css: {
            boolean: true,
            describe: 'output the required CSS rather than the SVG itself'
        },
        fontCache: {
            boolean: true,
            default: true,
            describe: 'whether to use a local font cache or not'
        }
    })
    .argv;

//
//  Create DOM adaptor and register it for HTML documents
//
const adaptor = new myAdaptor({
    cjkWidth: argv.cjkWidth,
    normalWidth: argv.normalWidth
});
RegisterHTMLHandler(adaptor);

//
//  Create input and output jax and a document using them on the content from the HTML file
//
const tex = new TeX({packages: argv.packages.split(/\s*,\s*/)});
const svg = new SVG({fontCache: (argv.fontCache ? 'local' : 'none')});
const html = mathjax.document('', {InputJax: tex, OutputJax: svg});

//
//  Typeset the math from the command line
//
const node = html.convert(argv._[0] || '', {
    display: !argv.inline,
    em: argv.em,
    ex: argv.ex,
    containerWidth: argv.width
});

//
//  If the --css option was specified, output the CSS,
//  Otherwise, typeset the math and output the HTML
//
if (argv.css) {
    console.log(adaptor.textContent(svg.styleSheet(html)));
} else {
    console.log(adaptor.outerHTML(node));
}

The main change is the addition of the myAdaptor class that implements the new measuring of unknown text. Note in particular the change in capitalization when loading LiteAdaptor rather than liteAdaptor in the require() statements at the top.

I also added command-line options to allow you to control the values without altering the program. These are passed to the new myAdaptor() call (that replaces liteAdaptor()) as options.

I think that should do what you want.

I'm working on mathjax-node v3, and also get stuck in this issue.

but i'm using this template of tex2svg-page https://github.com/mathjax/MathJax-demos-node/blob/master/component/tex2svg-page, really has no clue on how to modified it after reading your modification of another version. Would you pls help me on modifying this template, thanks.

@mathedu4all, The example that corresponds to the one I modified above is the direct tex2svg-page one. You should be able to make the corresponding changes there. Is there a reason that you need to use the component-based version rather than the direct one?

The changes you have made are basically just setting the options, and that can also be done in the direct version, but they just aren't all in one place. For example, the TeX input options are in the line

https://github.com/mathjax/MathJax-demos-node/blob/567dd623affac2830f6b3a8556c7ffc8e9d34fed/direct/tex2svg-page#L77 https://github.com/mathjax/MathJax-demos-node/blob/567dd623affac2830f6b3a8556c7ffc8e9d34fed/direct/tex2svg-page#L77

See if you are able to make that work for you.

thanks for your kind reply and contribution to the mathjax. It helps me a lot.

This works for me, but I'm curious what should be returned in nodeSize. The implementation of LiteAdaptor returns 0.6 for each character, but in the code above you use 8.5 for normal width and 13 for CJK width. This is a little confusing to me.

@perqin, you are right the code I posted is broken. The result should be the size in ems not in pixels, which is what my code returned. I have modified my example above (so that anyone copying will get the corrected version).

There was also a problem that the CJK width was being applied to ALL characters (that aren't in the MathJax fonts), not just the CJK ones. I've added a function to decide when to apply the CJK width. I'm not really sure which Unicode ranges should be included, but I think this should handle most situations. Modify the function as needed.

Sorry about the errors in the original. Hope this resolves any issues.

There is now a PR for this.

Was this page helpful?
0 / 5 - 0 ratings