/ chmod777_includes / html_css / element.lua
element.lua
  1  -- File: element.lua
  2  
  3  --[[
  4  Copyright (C) 2024 chmod777
  5  
  6  This program is free software: you can redistribute it and/or modify it under
  7  the terms of the GNU Affero General Public License version 3 as published by the
  8  Free Software Foundation.
  9  
 10  This program is distributed in the hope that it will be useful, but WITHOUT ANY
 11  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 12  PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
 13  
 14  You should have received a copy of the GNU Affero General Public License along
 15  with this program. If not, see <https://www.gnu.org/licenses/>. 
 16  ]]
 17  
 18  local luaWidgetDir = 'LuaUI/Widgets/'
 19  
 20  local Length = VFS.Include(luaWidgetDir..'chmod777_includes/html_css/css_property_parser.lua').Length
 21  
 22  local Initial = {
 23  	['display'] = 'block',
 24  	['box-sizing'] = 'content-box',
 25  
 26  	['width'] = 'auto',
 27  	['min-width'] = 'auto',
 28  	['max-width'] = 'none',
 29  	
 30  	['height'] = 'auto',
 31  	['min-height'] = 'auto',
 32  	['max-height'] = 'none',
 33  	
 34  	['padding-top'] = Length:new(0, "px"),
 35  	['padding-right'] = Length:new(0, "px"),
 36  	['padding-bottom'] = Length:new(0, "px"),
 37  	['padding-left'] = Length:new(0, "px"),
 38  
 39  	['border-top-width'] = Length:new(0, "px"),
 40  	['border-right-width'] = Length:new(0, "px"),
 41  	['border-bottom-width'] = Length:new(0, "px"),
 42  	['border-left-width'] = Length:new(0, "px"),
 43  
 44  	['margin-top'] = Length:new(0, "px"),
 45  	['margin-right'] = Length:new(0, "px"),
 46  	['margin-bottom'] = Length:new(0, "px"),
 47  	['margin-left'] = Length:new(0, "px"),
 48  
 49  	-- ['background-color'] = Color['transparent'](),
 50  	-- ['background-image'] = 'none',
 51  	-- ['background-size'] = BackgroundSize:new(),
 52  	-- ['background-position'] = BackgroundPosition:new(),
 53  	-- ['background-repeat'] = BackgroundRepeat:new(),
 54  	-- ['background-blend-mode'] = 'normal',
 55  
 56  	['div'] = {},
 57  	['span'] = {
 58  		['display'] = 'inline',
 59  	},
 60  }
 61  
 62  local Element = {}
 63  function Element:new(tag, attributes, children, text)
 64  	if tag == nil then tag = 'root' end
 65  	if attributes == nil then attributes = {} end
 66  	if children == nil then children = {} end
 67  	if text ~= nil then text = text:gsub("%s+$", "") end
 68  	local this = {
 69  		tag = tag,
 70  		attributes = attributes,
 71  		children = children,
 72  		text = text,
 73  		rulesets = {},
 74  	}
 75  	function this:link_elements(parent, previous_sibling, next_sibling)
 76  		this.parent = parent
 77  		this.previous_sibling = previous_sibling
 78  		this.next_sibling = next_sibling
 79  		for i,child in ipairs(children) do
 80  			child:link_elements(this, this.children[i-1], this.children[i+1])
 81  		end
 82  	end
 83  
 84  	function this:build_style_tree(stylesheet)
 85  		local style_tree = this
 86  		for r,ruleset in ipairs(stylesheet) do
 87  			local selector_group = ruleset.selector_group
 88  			local elements = style_tree:get_elements_by_selector_group(selector_group)
 89  			for e,element in ipairs(elements) do
 90  				element:add_ruleset(ruleset)
 91  				ruleset:add_element(element)
 92  			end
 93  		end
 94  		return style_tree
 95  	end
 96  	function this:add_ruleset(new_ruleset)
 97  		local rulesets = this.rulesets
 98  		for r,ruleset in pairs(rulesets) do
 99  			if new_ruleset == ruleset then return end
100  		end
101  		this.rulesets[#this.rulesets+1] = new_ruleset
102  	end
103  	function this:get_elements_by_selector_group(selector_group)
104  		local elements = {}
105  		for g,selector in ipairs(selector_group) do
106  			this:elements_by_selector(elements, selector, 1, g)
107  		end
108  		for c,child in ipairs(this.children) do
109  			local child_matches = child:get_elements_by_selector_group(selector_group)
110  			for i,child_match in ipairs(child_matches) do
111  				child_match:add_this_to_elements(elements)
112  			end
113  		end
114  		return elements
115  	end
116  	function this:elements_by_selector(elements, selector, current_i, group)
117  		local current_selector = selector[current_i]
118  		local current_type = current_selector.type
119  
120  		local current_selector_matches = this:simple_selector_matches(current_selector)
121  		local next_selector = selector[current_i+1]
122  		if current_selector_matches and next_selector then
123  			local combinator = current_selector.combinator
124  			if combinator == "SPACE" then -- descendant
125  				for child in ipairs(this.children) do
126  					Spring.Echo("Descendant combinator not implemented")
127  				end
128  			elseif combinator == ">" then -- child
129  				this:child_by_selector(elements, selector, current_i, group)
130  			elseif combinator == "+" then -- next-sibling
131  				this:next_sibling_by_selector(elements, selector, current_i, group)
132  			elseif combinator == "~" then -- subsequent-sibling
133  				this:subsequent_siblings_by_selector(elements, selector, current_i, group)
134  			else
135  				Spring.Echo("Unknown combinator!")
136  			end
137  		elseif current_selector_matches then -- and not next_selector
138  			this:add_this_to_elements(elements)
139  		elseif next_selector then -- and not current_selector_matches
140  		else -- not current_selector_matches and not next_selector
141  		end
142  	end
143  	function this:simple_selector_matches(simple_selector)
144  		local selector_type = simple_selector.type
145  		if selector_type == "univeral" then
146  			return true
147  		elseif selector_type == "type selector" then
148  			return this.tag == simple_selector.name
149  		elseif selector_type == "hash" then
150  			return this.attributes["id"] == simple_selector.name
151  		elseif selector_type == "class" then
152  			return this.attributes["class"] == simple_selector.name
153  		else
154  			Spring.Echo("Selector "..selector_type.." not implemented!")
155  			return false
156  		end
157  	end
158  	function this:add_this_to_elements(elements)
159  		-- Don't add duplicates
160  		for i=1, #elements do
161  			if this == elements[i] then return end
162  		end
163  		elements[#elements+1] = this
164  	end
165  	function this:child_by_selector(elements, selector, current_i, group)
166  		local next_selector = selector[current_i+1]
167  		for c,child in ipairs(this.children) do
168  			if child:simple_selector_matches(next_selector) then
169  				child:elements_by_selector(elements, selector, current_i+1, group)
170  			end
171  		end
172  	end
173  	function this:next_sibling_by_selector(elements, selector, current_i, group)
174  		local next_selector = selector[current_i+1]
175  		local next_sibling = this.next_sibling
176  		while next_sibling ~= nil do
177  			if next_sibling:simple_selector_matches(next_selector) then
178  				next_sibling:elements_by_selector(elements, selector, current_i+1, group)
179  				break
180  			end
181  			next_sibling = next_sibling.next_sibling
182  		end
183  	end
184  	function this:subsequent_siblings_by_selector(elements, selector, current_i, group)
185  		local next_selector = selector[current_i+1]
186  		local next_sibling = this.next_sibling
187  		while next_sibling ~= nil do
188  			if next_sibling:simple_selector_matches(next_selector) then
189  				next_sibling:elements_by_selector(elements, selector, current_i+1, group)
190  			end
191  			next_sibling = next_sibling.next_sibling
192  		end
193  	end
194  
195  	function this:get_declaration(property_name)
196  		local value
197  		for r,ruleset in ipairs(this.rulesets) do
198  			for d,declaration in ipairs(ruleset) do
199  				if property_name == declaration.property then
200  					value = declaration.value
201  				end
202  			end
203  		end
204  		return value
205  	end
206  	function this:get_initial(property_name)
207  		return Initial[this.tag][property_name]
208  	end
209  
210  	function this:get_value_or(property_name, default)
211  		local declaration = this:get_declaration(property_name)
212  		if declaration == nil then
213  			return default
214  		end
215  		if type(declaration) == "table" then
216  			return declaration.value
217  		end
218  		return declaration
219  	end
220  	function this:get_absolute_value_or(property_name, default, context)
221  		local declaration = this:get_declaration(property_name)
222  		if declaration == nil then
223  			return default
224  		end
225  		return declaration.value:as_px(context).value
226  	end
227  
228  	function this:print(tabs)
229  		if tabs == nil then tabs = 0 end
230  		local prefix = ""
231  		for tab=1, tabs do
232  			prefix = prefix.."\t"
233  		end
234  		Spring.Echo(prefix.."{")
235  		Spring.Echo(prefix.."tag:", this.tag)
236  		Spring.Echo(prefix.."text:", this.text)
237  		Spring.Echo(prefix.."attributes:")
238  		for k,v in pairs(this.attributes) do
239  			Spring.Echo("\t"..prefix..k,v)
240  		end
241  		Spring.Echo(prefix.."children:")
242  		for k,v in pairs(this.children) do
243  			v:print(tabs+1)
244  		end
245  		Spring.Echo(prefix.."rules")
246  		for k,v in pairs(this.rulesets) do
247  			for k,v in pairs(v) do
248  				Spring.Echo(k,v.property, v.value)
249  			end
250  		end
251  
252  		Spring.Echo(prefix.."}")
253  	end
254  
255  	return this
256  end
257  
258  return {
259  	Initial = Initial,
260  	Element = Element,
261  }