DEV Community

Murahashi [Matt] Kenichi
Murahashi [Matt] Kenichi

Posted on • Updated on

Let's build browser engine! in typescript vol9 DOM and Rules to Style tree

Transform DOM node and Rules to StyleTree!


test("matchRule none-match", () => {
  expect(matchRule(new ElementData("no mean", new Map([])), new Rule([], []))).toBeNull();
});

test("matchRule match first", () => {
  const rule = new Rule(
    [
      // specificity a=1, b=0, c=0
      new Selector.Simple(new SimpleSelector(null, "target", [])),
      // specificity a=0, b=0, c=1
      new Selector.Simple(new SimpleSelector("target", null, []))
    ],
    []
  );
  expect(matchRule(new ElementData("target", new Map([["id", "target"]])), rule)).toEqual([
    [1, 0, 0],
    rule
  ]);
});
Enter fullscreen mode Exit fullscreen mode
export type MatchedRule = [Specificity, Rule];

// If `rule` matches `elem`, return a `MatchedRule`. Otherwise return null.
export function matchRule(elem: ElementData, rule: Rule): null | MatchedRule {
  // Find the first (most specific) matching selector.
  // Because our CSS parser stores the selectors from most- to least-specific,
  const found = rule.selectors.find(selector => {
    return matches(elem, selector);
  });
  if (found === undefined) {
    return null;
  }
  return [found.selector.specificity(), rule];
}
Enter fullscreen mode Exit fullscreen mode

test("matchingRules none-match", () => {
  expect(matchingRules(new ElementData("no mean", new Map([])), new Stylesheet([]))).toEqual([]);
});

test("matchingRules matches", () => {
  const rule1 = new Rule(
    [
      // specificity a=1, b=0, c=0
      new Selector.Simple(new SimpleSelector(null, "target", [])),
      // specificity a=0, b=0, c=1
      new Selector.Simple(new SimpleSelector("target", null, []))
    ],
    []
  );
  const rule2 = new Rule(
    [
      // specificity a=0, b=0, c=1
      new Selector.Simple(new SimpleSelector("target", null, []))
    ],
    []
  );
  expect(
    matchingRules(
      new ElementData("target", new Map([["id", "target"]])),
      new Stylesheet([rule1, rule2])
    )
  ).toEqual([[[1, 0, 0], rule1], [[0, 0, 1], rule2]]);
});
Enter fullscreen mode Exit fullscreen mode
export function matchingRules(elem: ElementData, stylesheet: Stylesheet): MatchedRule[] {
  return stylesheet.rules
    .map(rule => {
      return matchRule(elem, rule);
    })
    .filter(
      (matchedOrNull): matchedOrNull is MatchedRule => {
        return matchedOrNull !== null;
      }
    );
}
Enter fullscreen mode Exit fullscreen mode

test("specifiedValues none", () => {
  expect(specifiedValues(new ElementData("no mean", new Map([])), new Stylesheet([]))).toEqual(
    new Map([])
  );
});

test("specifiedValues", () => {
  const rule1 = new Rule(
    [
      // specificity a=1, b=0, c=0
      new Selector.Simple(new SimpleSelector(null, "target", [])),
      // specificity a=0, b=0, c=1
      new Selector.Simple(new SimpleSelector("target", null, []))
    ],
    [
      new Declaration("override", new CssValue.Keyword("current")),
      new Declaration("not-override1", new CssValue.Keyword("value1"))
    ]
  );
  const rule2 = new Rule(
    [
      // specificity a=0, b=0, c=1
      new Selector.Simple(new SimpleSelector("target", null, []))
    ],
    [
      new Declaration("override", new CssValue.Keyword("prev")),
      new Declaration("not-override2", new CssValue.Keyword("value2"))
    ]
  );
  expect(
    specifiedValues(
      new ElementData("target", new Map([["id", "target"]])),
      new Stylesheet([rule1, rule2])
    )
  ).toEqual(
    new Map([
      ["not-override1", new CssValue.Keyword("value1")],
      ["not-override2", new CssValue.Keyword("value2")],
      ["override", new CssValue.Keyword("current")]
    ])
  );
});

test("compare matched rule right a", () => {
  expect(
    compareMatchedRule([[0, 0, 0], new Rule([], [])], [[1, 0, 0], new Rule([], [])])
  ).toBeLessThan(0);
});

test("compare matched rule left a", () => {
  expect(
    compareMatchedRule([[1, 0, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
  ).toBeGreaterThan(0);
});

test("compare matched rule right b", () => {
  expect(
    compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 1, 0], new Rule([], [])])
  ).toBeLessThan(0);
});

test("compare matched rule left b", () => {
  expect(
    compareMatchedRule([[0, 1, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
  ).toBeGreaterThan(0);
});

test("compare matched rule right c", () => {
  expect(
    compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 0, 1], new Rule([], [])])
  ).toBeLessThan(0);
});

test("compare matched rule left c", () => {
  expect(
    compareMatchedRule([[0, 0, 1], new Rule([], [])], [[0, 0, 0], new Rule([], [])])
  ).toBeGreaterThan(0);
});

test("compare matched rule same", () => {
  expect(compareMatchedRule([[0, 0, 0], new Rule([], [])], [[0, 0, 0], new Rule([], [])])).toBe(0);
});

test("sort compare matched rule", () => {
  const left: MatchedRule = [[0, 0, 0], new Rule([], [])];
  const right: MatchedRule = [[1, 0, 0], new Rule([], [])];
  expect([left, right].sort(compareMatchedRule)).toEqual([left, right]);
});
Enter fullscreen mode Exit fullscreen mode

export function compareMatchedRule(left: MatchedRule, right: MatchedRule): number {
  const [[la, lb, lc]] = left;
  const [[ra, rb, rc]] = right;
  if (la !== ra) {
    return la - ra;
  } else if (lb !== rb) {
    return lb - rb;
  } else if (lc !== rc) {
    return lc - rc;
  }
  return 0;
}

// Apply styles to a single element, returning the specified styles.
//
// To do: Allow multiple UA/author/user stylesheets, and implement the cascade.
export function specifiedValues(elem: ElementData, stylesheet: Stylesheet): PropertyMap {
  const values = new Map<string, CssValue>([]);
  const rules = matchingRules(elem, stylesheet);
  rules.sort(compareMatchedRule);
  for (const [, rule] of rules) {
    for (const declaration of rule.declarations) {
      values.set(declaration.name, declaration.value);
    }
  }
  return values;
}
Enter fullscreen mode Exit fullscreen mode
test("style node text", () => {
  expect(styleTree(text("hoge"), new Stylesheet([]))).toEqual(
    new StyledNode(text("hoge"), new Map([]), [])
  );
});

test("style node element", () => {
  const rule = new Rule(
    [new Selector.Simple(new SimpleSelector(null, "target", []))],
    [new Declaration("some", new CssValue.Keyword("foo"))]
  );
  const element = elem("no mean", new Map([["id", "target"]]), []);
  expect(styleTree(element, new Stylesheet([rule]))).toEqual(
    new StyledNode(element, new Map([["some", new CssValue.Keyword("foo")]]), [])
  );
});

test("style node children", () => {
  const rule = new Rule(
    [new Selector.Simple(new SimpleSelector(null, "target", []))],
    [new Declaration("some", new CssValue.Keyword("foo"))]
  );
  const element = elem("no mean", new Map([]), [elem("no mean", new Map([["id", "target"]]), [])]);
  expect(styleTree(element, new Stylesheet([rule]))).toEqual(
    new StyledNode(element, new Map([]), [
      new StyledNode(
        elem("no mean", new Map([["id", "target"]]), []),
        new Map([["some", new CssValue.Keyword("foo")]]),
        []
      )
    ])
  );
});
Enter fullscreen mode Exit fullscreen mode

// Apply a stylesheet to an entire DOM tree, returning a StyledNode tree.
export function styleTree(root: DomNode, stylesheet: Stylesheet): StyledNode {
  switch (root.nodeType.format) {
    case NodeType.Format.Text:
      return new StyledNode(
        root,
        new Map([]),
        // NOTE: text node has children??? I'm not sure
        []
      );
    case NodeType.Format.Element:
      return new StyledNode(
        root,
        specifiedValues(root.nodeType.element, stylesheet),
        root.children.map(child => {
          return styleTree(child, stylesheet);
        })
      );
  }
}
Enter fullscreen mode Exit fullscreen mode

references

series

Top comments (0)