Skip to content

Conversation

asturur
Copy link
Member

@asturur asturur commented May 26, 2025

Description

closes #10635

In Action

Copy link

codesandbox bot commented May 26, 2025

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

Copy link
Contributor

github-actions bot commented May 26, 2025

Build Stats

file / KB (diff) bundled minified
fabric 916.091 (+0.045) 301.478 (+0.017)

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

does this work with a viewbox like
element.setAttribute('viewBox', '0,0 100,50');

is this even valid?
I think web allows mixed separators
an example test case

it('viewBox with mixed separators', () => {
      const element = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'svg',
      );
      element.setAttribute('viewBox', '0,0 100,50');
      element.setAttribute('width', '200');
      element.setAttribute('height', '100');

      const result = applyViewboxTransform(element);

      expect(result.viewBoxWidth).toBe(100);
      expect(result.viewBoxHeight).toBe(50);
    });

@asturur
Copy link
Member Author

asturur commented May 26, 2025

does this work with a viewbox like element.setAttribute('viewBox', '0,0 100,50');

is this even valid? I think web allows mixed separators an example test case

it('viewBox with mixed separators', () => {
      const element = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'svg',
      );
      element.setAttribute('viewBox', '0,0 100,50');
      element.setAttribute('width', '200');
      element.setAttribute('height', '100');

      const result = applyViewboxTransform(element);

      expect(result.viewBoxWidth).toBe(100);
      expect(result.viewBoxHeight).toBe(50);
    });

is that on spec? if is on spec it should work. let's add a test case for it.

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/viewBox
It has this part

The value of the viewBox attribute is a list of four numbers separated by whitespace and/or a comma: min-x, min-y, width, and height

the part and/or threw me off, meaning it allows mixed stuff

@asturur
Copy link
Member Author

asturur commented May 26, 2025

0,0 100,50

Yes SVG is crazy specs.
Write numbers however you want as long they are really super wrong and they should work. Not sure why.
But this wasn't working even before my changes, was it?

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

0,0 100,50

Yes SVG is crazy specs. Write numbers however you want as long they are really super wrong and they should work. Not sure why. But this wasn't working even before my changes, was it?

apparently not
I think we need a really crazy regex here

export const reViewBoxAttrValue = new RegExp(
  '^\\s*' +
  '(' + reNum + ')' +
  '(?:\\s*,?\\s+|\\s*,\\s*)' + 
  '(' + reNum + ')' +
  '(?:\\s*,?\\s+|\\s*,\\s*)' +
  '(' + reNum + ')' +
  '(?:\\s*,?\\s+|\\s*,\\s*)' +
  '(' + reNum + ')' +
  '\\s*$'
);

maybe this

@asturur
Copy link
Member Author

asturur commented May 26, 2025

i don't get it, the comma is there.

@asturur
Copy link
Member Author

asturur commented May 26, 2025

image

I do not understand why the space make it pass.

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

image I do not understand why the space make it pass.

have you tried the one I suggested above?

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

also I think we should not just test these constants but fully the applyViewBoxTransform file
we can add applyViewboxTransform.spec.ts file and then we test the full pipeline through it

here are some test cases I came up with

describe('applyViewboxTransform()', () => {
  it('return empty object for non-viewBox elements', () => {
    const element = document.createElement('rect');
    const result = applyViewboxTransform(element);
    expect(result).toEqual({});
  });

  it('return empty object for unsupported elements', () => {
    const element = document.createElement('circle');
    const result = applyViewboxTransform(element);
    expect(result).toEqual({});
  });

  it('handle missing viewBox with valid dimensions', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('width', '100');
    element.setAttribute('height', '200');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 200,
    });
  });

  it('handle missing viewBox and missing dimensions', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 0,
      height: 0,
    });
  });

  it('handle missing viewBox with percentage dimensions', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('width', '100%');
    element.setAttribute('height', '100%');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 0,
      height: 0,
    });
  });

  it('handle x/y translation for missing viewBox', () => {
    const parent = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'symbol',
    );
    parent.appendChild(element);

    element.setAttribute('x', '10');
    element.setAttribute('y', '20');
    element.setAttribute('width', '100');
    element.setAttribute('height', '200');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 200,
    });
    expect(element.getAttribute('transform')).toContain('translate(10 20)');
    expect(element.hasAttribute('x')).toBe(false);
    expect(element.hasAttribute('y')).toBe(false);
  });

  it('handles invalid viewBox with 3 values', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0 0 408');
    element.setAttribute('width', '100');
    element.setAttribute('height', '200');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 200,
    });
  });

  it('handle invalid viewBox with malformed values', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', 'invalid values here');
    element.setAttribute('width', '100');
    element.setAttribute('height', '200');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 200,
    });
  });

  it('handle empty viewBox', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '');
    element.setAttribute('width', '100');
    element.setAttribute('height', '200');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 200,
    });
  });

  it('parse valid viewBox with dimensions', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 200,
      height: 100,
      minX: 0,
      minY: 0,
      viewBoxWidth: 100,
      viewBoxHeight: 50,
    });
  });

  it('parse valid viewBox without dimensions', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '10 20 100 50');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 50,
      minX: -10,
      minY: -20,
      viewBoxWidth: 100,
      viewBoxHeight: 50,
    });
  });

  it('handle negative viewBox values', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '-10 -20 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 200,
      height: 100,
      minX: 10,
      minY: 20,
      viewBoxWidth: 100,
      viewBoxHeight: 50,
    });
  });

  it('handle decimal viewBox values', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0.5 1.5 100.5 50.5');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 200,
      height: 100,
      minX: -0.5,
      minY: -1.5,
      viewBoxWidth: 100.5,
      viewBoxHeight: 50.5,
    });
  });

  it('not generate transform for identity case', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '100');
    element.setAttribute('height', '50');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 100,
      height: 50,
      minX: 0,
      minY: 0,
      viewBoxWidth: 100,
      viewBoxHeight: 50,
    });
    expect(element.getAttribute('transform')).toBeFalsy();
  });

  it('generate transform matrix for scaling', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    applyViewboxTransform(element);

    const gElement = element.querySelector('g');
    expect(gElement).toBeTruthy();
    expect(gElement?.getAttribute('transform')).toContain(
      'matrix(2 0 0 2 0 0)',
    );
  });

  it('generate transform matrix with translation', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '10 20 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    applyViewboxTransform(element);

    const gElement = element.querySelector('g');
    expect(gElement?.getAttribute('transform')).toContain(
      'matrix(2 0 0 2 -20 -40)',
    );
  });

  it('handle x/y attributes with viewBox', () => {
    const parent = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'symbol',
    );
    parent.appendChild(element);

    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');
    element.setAttribute('x', '5');
    element.setAttribute('y', '10');

    applyViewboxTransform(element);

    expect(element.getAttribute('transform')).toContain('translate(5 10)');
    expect(element.getAttribute('transform')).toContain('matrix(2 0 0 2 0 0)');
  });

  it('create g wrapper for svg elements', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const child = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'rect',
    );
    element.appendChild(child);

    applyViewboxTransform(element);

    const gElement = element.querySelector('g');
    expect(gElement).toBeTruthy();
    expect(gElement?.contains(child)).toBe(true);
    expect(element.children.length).toBe(1);
    expect(element.children[0]).toBe(gElement);
  });

  it('apply transform directly to non-svg elements', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'symbol',
    );
    element.setAttribute('viewBox', '0 0 100 50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');
    element.setAttribute('transform', 'rotate(45)');

    applyViewboxTransform(element);

    expect(element.getAttribute('transform')).toContain('rotate(45)');
    expect(element.getAttribute('transform')).toContain('matrix(2 0 0 2 0 0)');
    expect(element.querySelector('g')).toBeFalsy();
  });

  it('handle scientific notation in viewBox', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '1e-2 2e-3 1e2 5e1');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result).toEqual({
      width: 200,
      height: 100,
      minX: -0.01,
      minY: -0.002,
      viewBoxWidth: 100,
      viewBoxHeight: 50,
    });
  });

  it('viewBox with mixed separators', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '0,0 100,50');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result.viewBoxWidth).toBe(100);
    expect(result.viewBoxHeight).toBe(50);
  });

  it('handle whitespace in viewBox', () => {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      'svg',
    );
    element.setAttribute('viewBox', '  0   0   100   50  ');
    element.setAttribute('width', '200');
    element.setAttribute('height', '100');

    const result = applyViewboxTransform(element);

    expect(result.viewBoxWidth).toBe(100);
    expect(result.viewBoxHeight).toBe(50);
  });
});

not sure if these all are valid test cases but we could pick some out

@asturur
Copy link
Member Author

asturur commented May 26, 2025

yeah i could test higher level, but now i m changing the regex, i test the regex. I can copy paste those tests somewhere else but when i do those changes i have to hope other unit tests would catch regressions. visual or unit

@asturur
Copy link
Member Author

asturur commented May 26, 2025

oh i see the issue, some spaces are optional, other are mandatory. that is wrong. all spaces are optional

@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

yeah i could test higher level, but now i m changing the regex, i test the regex. I can copy paste those tests somewhere else but when i do those changes i have to hope other unit tests would catch regressions. visual or unit

Yeah the tests i proposed are for a new file where apply viewbox lives
They all pass for me with the regex i proposed above

@asturur
Copy link
Member Author

asturur commented May 26, 2025

apply viewbox transform is such a confusing function :(
it change the element and returns data, it was my early js coding and trying to save any byte where possible

@asturur asturur merged commit 6a60f29 into master May 26, 2025
13 checks passed
@asturur asturur deleted the fix-viewbox-attr branch May 26, 2025 14:40
@Smrtnyk
Copy link
Contributor

Smrtnyk commented May 26, 2025

apply viewbox transform is such a confusing function :( it change the element and returns data, it was my early js coding and trying to save any byte where possible

Yes, that is why i proposed more compound tests, because i wasnt sure if these return values were actually corect, mainly these with -0

@asturur
Copy link
Member Author

asturur commented May 27, 2025

Yeah no sure why the minus. i forgot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: "viewbox" attribute regular expression parsing issue
2 participants