citrine 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/citrine.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Citrine — 信号式 Ruby UI 框架(黄水晶:水晶振荡器是信号源,命名致敬 Opal)
4
+ # 设计定案见 GOALS.md 第七节:Signal 三宏 + block 级细粒度更新。
5
+ # 本文件只加载平台无关核心;浏览器入口见 citrine/browser.rb。
6
+
7
+ module Citrine
8
+ class << self
9
+ # 当前渲染器(由具体平台入口设置,如 citrine/dom.rb 的 DomRenderer)
10
+ attr_accessor :renderer
11
+
12
+ # 开发模式:只影响提示/诊断输出,不改变渲染语义。
13
+ # `bin/citrine dev` 打开的页面由 dev_server 注入 window.CITRINE_DEV 自动置位;
14
+ # CRuby 侧(SSR / 单测)可用 `Citrine.dev_mode = true` 手动打开。
15
+ attr_writer :dev_mode
16
+
17
+ def dev_mode?
18
+ !!@dev_mode
19
+ end
20
+
21
+ def mount(component, element)
22
+ raise "Citrine.renderer 未设置(浏览器入口应 require \"citrine/browser\")" unless renderer
23
+
24
+ renderer.mount_component(component, element)
25
+ end
26
+
27
+ # 卸载一个已挂载的组件(会跑 on_unmount、解绑全局键盘、销毁所有 Effect)
28
+ def unmount(component)
29
+ root = component.respond_to?(:root) ? component.root : component
30
+ raise ArgumentError, "Citrine.unmount:组件尚未挂载(root 为空)" unless root
31
+
32
+ renderer = component.respond_to?(:renderer) && component.renderer ? component.renderer : self.renderer
33
+ raise "Citrine.unmount:找不到挂载这个组件的渲染器" unless renderer
34
+
35
+ renderer.unmount_component(root)
36
+ end
37
+
38
+ # render-to-string(纯 CRuby 可用)
39
+ def render(component)
40
+ require_relative "citrine/string_renderer"
41
+ StringRenderer.render(component)
42
+ end
43
+
44
+ # 批量窗口(S1-1):块内对信号的多次写入合并为一轮 Effect 重跑,
45
+ # 中间态不进 DOM。事件处理器的分发过程已自动包裹一次;手动改多个信号
46
+ # 又不想看到级联重渲染时用:
47
+ #
48
+ # Citrine.batch do
49
+ # self.a = 1
50
+ # self.b = 2 # a、b 的读者各只重跑一次
51
+ # end
52
+ def batch(&block)
53
+ Scheduler.batch(&block)
54
+ end
55
+
56
+ # 造一个新信号。模块级工厂,哪儿都能用(领域模型 / 测试 / 组件外):
57
+ #
58
+ # tick = Citrine.signal(0)
59
+ # rows = Citrine.signal { load_rows } # 块 = 惰性初值,第一次读取时求值一次
60
+ #
61
+ # 存在的理由是"不让人写出裸的 `Signal`"——stdlib 与 Opal corelib 都有
62
+ # `::Signal`(进程信号),裸写会拿到那个类,报错完全不指向真因(FRICTION F14)。
63
+ # 普通类里想少打字可以 `include Citrine::Reactive`,得到同名实例方法。
64
+ def signal(value = nil, &init)
65
+ Signal.new(value, &init)
66
+ end
67
+
68
+ # 造一个响应式集合(D):集合自身的每次变更都是一次通知,写法保持集合的样子。
69
+ #
70
+ # rows = Citrine.signal_list([])
71
+ # rows << row
72
+ # rows.delete_at(0)
73
+ #
74
+ # get 返回**冻结**快照:`rows.get << x` 会当场 FrozenError,而不是静默不触发。
75
+ def signal_list(items = [])
76
+ ListSignal.new(items)
77
+ end
78
+ end
79
+ end
80
+
81
+ require_relative "citrine/version"
82
+ require_relative "citrine/theme"
83
+ require_relative "citrine/signal"
84
+ require_relative "citrine/list_signal"
85
+ require_relative "citrine/reactive"
86
+ require_relative "citrine/key_event"
87
+ require_relative "citrine/event"
88
+ require_relative "citrine/sourcemap"
89
+ require_relative "citrine/num"
90
+ require_relative "citrine/node"
91
+ require_relative "citrine/component"
92
+ require_relative "citrine/renderer"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Citrine
8
+ # Citrine 组件里的裸 @ivar 赋值绕过信号追踪——写入不会触发任何更新,
9
+ # 是"改了但界面不动"这类事故的根源(GOALS.md 第七节代价清单第 1 条)。
10
+ #
11
+ # 状态请用 `state` 宏(赋值即更新),跨渲染的派生值用 `computed`,
12
+ # 单次渲染内的中间值用局部变量。
13
+ #
14
+ # class Counter < Citrine::Component
15
+ # state :count, default: 0 # ✓
16
+ # @cache = {} # ✗ 裸 ivar 赋值
17
+ # end
18
+ #
19
+ # 只拦"写入":裸读(@foo)在框架语义里无法区分,交由 code review。
20
+ class NoRawIvarAssignment < RuboCop::Cop::Base
21
+ MSG = "Citrine 组件内不要直接赋值 @ivar(绕过信号追踪):状态用 state 宏,中间值用局部变量"
22
+
23
+ # 类体上的 @ivar 赋值(如宏实现的内部状态)不属于组件实例,不拦;
24
+ # 框架源码(lib/citrine/**)整体豁免,见 .rubocop.yml
25
+ def on_ivasgn(node)
26
+ return unless inside_component_scope?(node)
27
+
28
+ add_offense(node.loc.name)
29
+ end
30
+
31
+ private
32
+
33
+ # 词法上落在 Citrine::Component 子类(具名类或 Class.new(Citrine::Component))
34
+ # 的**方法体**里的赋值。类体上的直接赋值是"组件类"状态(宏实现的内部
35
+ # 状态就是这种写法),不属于组件实例,不拦。
36
+ def inside_component_scope?(node)
37
+ node.each_ancestor(:class, :block).any? do |scope|
38
+ case scope.type
39
+ when :class
40
+ component_superclass?(scope.children[1]) && defined_in_method?(node, scope)
41
+ when :block
42
+ # (block (send Class :new <Component>) ...) → children[0] 是调用节点
43
+ class_new_component?(scope.children[0]) && defined_in_method?(node, scope)
44
+ else false
45
+ end
46
+ end
47
+ end
48
+
49
+ def defined_in_method?(node, class_scope)
50
+ method_scope = node.each_ancestor(:def, :defs).first
51
+ return false unless method_scope
52
+
53
+ # def 最近的词法作用域必须就是该组件作用域
54
+ #(匿名类没有 :class 节点,最近作用域是 Class.new 的 :block)
55
+ nearest = method_scope.each_ancestor(:class, :sclass, :module, :block).first
56
+ nearest&.equal?(class_scope)
57
+ end
58
+
59
+ def component_superclass?(superclass_node)
60
+ return false unless superclass_node&.const_type?
61
+
62
+ %w[Citrine::Component Component].include?(superclass_node.const_name)
63
+ end
64
+
65
+ def class_new_component?(send_node)
66
+ return false unless send_node&.method?(:new) && send_node.receiver&.const_type?
67
+ return false unless send_node.receiver.const_name == "Class"
68
+
69
+ base = send_node.arguments.first
70
+ base&.const_type? && %w[Citrine::Component Component].include?(base.const_name)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: citrine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - ShiningRay
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: puma
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: listen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: opal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
97
+ description: Citrine(黄水晶):信号式响应 UI 框架。用纯 Ruby 写组件(state / computed 宏 + 赋值即更新),经 Opal
98
+ 编译后渲染到浏览器 DOM、Canvas 与桌面(macOS .app)。水晶振荡器是信号的源头——名字致敬 Opal 开启的 Ruby→Web 宝石谱系。
99
+ email:
100
+ - shiningray@users.noreply.github.com
101
+ executables:
102
+ - citrine
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - LICENSE
107
+ - README.md
108
+ - bin/citrine
109
+ - desktop/main.swift
110
+ - lib/citrine.rb
111
+ - lib/citrine/browser.rb
112
+ - lib/citrine/canvas.rb
113
+ - lib/citrine/component.rb
114
+ - lib/citrine/debug.rb
115
+ - lib/citrine/dev_server.rb
116
+ - lib/citrine/dom.rb
117
+ - lib/citrine/event.rb
118
+ - lib/citrine/key_event.rb
119
+ - lib/citrine/list_signal.rb
120
+ - lib/citrine/node.rb
121
+ - lib/citrine/num.rb
122
+ - lib/citrine/packager.rb
123
+ - lib/citrine/reactive.rb
124
+ - lib/citrine/renderer.rb
125
+ - lib/citrine/signal.rb
126
+ - lib/citrine/sourcemap.rb
127
+ - lib/citrine/string_renderer.rb
128
+ - lib/citrine/style.rb
129
+ - lib/citrine/theme.rb
130
+ - lib/citrine/version.rb
131
+ - lib/rubocop/cop/citrine/no_raw_ivar_assignment.rb
132
+ homepage: https://github.com/ShiningRay/citrine
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ source_code_uri: https://github.com/ShiningRay/citrine
137
+ changelog_uri: https://github.com/ShiningRay/citrine/blob/main/GOALS.md
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 3.0.0
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubygems_version: 3.5.22
154
+ signing_key:
155
+ specification_version: 4
156
+ summary: Signal-based reactive UI framework for Ruby, powered by Opal
157
+ test_files: []