1import { getResourceXMLPathAsync } from './Paths';
2import {
3  buildResourceGroup,
4  buildResourceItem,
5  ensureDefaultResourceXML,
6  findResourceGroup,
7  getResourceItemsAsObject,
8  readResourcesXMLAsync,
9  ResourceGroupXML,
10  ResourceItemXML,
11  ResourceKind,
12  ResourceXML,
13} from './Resources';
14
15// Adds support for `tools:x`
16const fallbackResourceString = `<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"></resources>`;
17
18export async function readStylesXMLAsync({
19  path,
20  fallback = fallbackResourceString,
21}: {
22  path: string;
23  fallback?: string | null;
24}): Promise<ResourceXML> {
25  return readResourcesXMLAsync({ path, fallback });
26}
27
28export async function getProjectStylesXMLPathAsync(
29  projectRoot: string,
30  { kind }: { kind?: ResourceKind } = {}
31): Promise<string> {
32  return getResourceXMLPathAsync(projectRoot, { kind, name: 'styles' });
33}
34
35function ensureDefaultStyleResourceXML(xml: ResourceXML): ResourceXML {
36  xml = ensureDefaultResourceXML(xml);
37  if (!Array.isArray(xml?.resources?.style)) {
38    xml.resources.style = [];
39  }
40  return xml;
41}
42
43export function getStyleParent(
44  xml: ResourceXML,
45  group: { name: string; parent?: string }
46): ResourceGroupXML | null {
47  return findResourceGroup(xml.resources.style, group);
48}
49
50export function getStylesItem({
51  name,
52  xml,
53  parent,
54}: {
55  name: string;
56  xml: ResourceXML;
57  parent: { name: string; parent?: string };
58}): ResourceItemXML | null {
59  xml = ensureDefaultStyleResourceXML(xml);
60
61  const appTheme = getStyleParent(xml, parent);
62
63  if (!appTheme) {
64    return null;
65  }
66
67  if (appTheme.item) {
68    const existingItem = appTheme.item.filter(({ $: head }) => head.name === name)[0];
69
70    // Don't want to 2 of the same item, so if one exists, we overwrite it
71    if (existingItem) {
72      return existingItem;
73    }
74  }
75  return null;
76}
77
78export function setStylesItem({
79  item,
80  xml,
81  parent,
82}: {
83  item: ResourceItemXML;
84  xml: ResourceXML;
85  parent: { name: string; parent: string };
86}): ResourceXML {
87  xml = ensureDefaultStyleResourceXML(xml);
88
89  let appTheme = getStyleParent(xml, parent);
90
91  if (!appTheme) {
92    appTheme = buildResourceGroup(parent);
93    xml.resources!.style!.push(appTheme);
94  }
95
96  if (appTheme.item) {
97    const existingItem = appTheme.item.filter(({ $: head }) => head.name === item.$.name)[0];
98
99    // Don't want to 2 of the same item, so if one exists, we overwrite it
100    if (existingItem) {
101      existingItem._ = item._;
102      existingItem.$ = item.$;
103    } else {
104      appTheme.item.push(item);
105    }
106  } else {
107    appTheme.item = [item];
108  }
109  return xml;
110}
111
112export function removeStylesItem({
113  name,
114  xml,
115  parent,
116}: {
117  name: string;
118  xml: ResourceXML;
119  parent: { name: string; parent: string };
120}): ResourceXML {
121  xml = ensureDefaultStyleResourceXML(xml);
122  const appTheme = getStyleParent(xml, parent);
123  if (appTheme?.item) {
124    const index = appTheme.item.findIndex(({ $: head }: ResourceItemXML) => head.name === name);
125    if (index > -1) {
126      appTheme.item.splice(index, 1);
127    }
128  }
129  return xml;
130}
131
132// This is a very common theme so make it reusable.
133export function getAppThemeLightNoActionBarGroup() {
134  return { name: 'AppTheme', parent: 'Theme.AppCompat.Light.NoActionBar' };
135}
136
137export function assignStylesValue(
138  xml: ResourceXML,
139  {
140    add,
141    value,
142    targetApi,
143    name,
144    parent,
145  }: {
146    add: boolean;
147    value: string;
148    targetApi?: string;
149    name: string;
150    parent: { name: string; parent: string };
151  }
152): ResourceXML {
153  if (add) {
154    return setStylesItem({
155      xml,
156      parent,
157      item: buildResourceItem({
158        name,
159        targetApi,
160        value,
161      }),
162    });
163  }
164  return removeStylesItem({
165    xml,
166    parent,
167    name,
168  });
169}
170
171/**
172 * Helper to convert a styles.xml parent's children into a simple k/v pair.
173 * Added for testing purposes.
174 *
175 * @param xml
176 * @returns
177 */
178export function getStylesGroupAsObject(
179  xml: ResourceXML,
180  group: { name: string; parent?: string }
181): Record<string, string> | null {
182  const xmlGroup = getStyleParent(xml, group);
183  return xmlGroup?.item ? getResourceItemsAsObject(xmlGroup.item) : null;
184}
185