mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 13:36:59 +01:00
Add static attributes
This commit is contained in:
parent
4be9831edd
commit
82c0922cfa
7 changed files with 116 additions and 91 deletions
|
|
@ -3,7 +3,7 @@ package scalive
|
||||||
object DiffBuilder:
|
object DiffBuilder:
|
||||||
def build(
|
def build(
|
||||||
static: Seq[String],
|
static: Seq[String],
|
||||||
dynamic: Seq[LiveMod[?]],
|
dynamic: Seq[LiveDyn[?]],
|
||||||
includeUnchanged: Boolean = false
|
includeUnchanged: Boolean = false
|
||||||
): Diff.Tag =
|
): Diff.Tag =
|
||||||
Diff.Tag(
|
Diff.Tag(
|
||||||
|
|
@ -11,16 +11,16 @@ object DiffBuilder:
|
||||||
dynamic = dynamic.zipWithIndex
|
dynamic = dynamic.zipWithIndex
|
||||||
.filter(includeUnchanged || _._1.wasUpdated)
|
.filter(includeUnchanged || _._1.wasUpdated)
|
||||||
.map {
|
.map {
|
||||||
case (v: LiveMod.Dynamic[?, ?], i) =>
|
case (v: LiveDyn.Value[?, ?], i) =>
|
||||||
Diff.Dynamic(i, Diff.Static(v.currentValue.toString))
|
Diff.Dynamic(i, Diff.Static(v.currentValue.toString))
|
||||||
case (v: LiveMod.When[?], i) => build(v, i, includeUnchanged)
|
case (v: LiveDyn.When[?], i) => build(v, i, includeUnchanged)
|
||||||
case (v: LiveMod.Split[?, ?], i) =>
|
case (v: LiveDyn.Split[?, ?], i) =>
|
||||||
Diff.Dynamic(i, build(v, includeUnchanged))
|
Diff.Dynamic(i, build(v, includeUnchanged))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
mod: LiveMod.When[?],
|
mod: LiveDyn.When[?],
|
||||||
index: Int,
|
index: Int,
|
||||||
includeUnchanged: Boolean
|
includeUnchanged: Boolean
|
||||||
): Diff.Dynamic =
|
): Diff.Dynamic =
|
||||||
|
|
@ -46,7 +46,7 @@ object DiffBuilder:
|
||||||
else Diff.Dynamic(index, Diff.Deleted)
|
else Diff.Dynamic(index, Diff.Deleted)
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
mod: LiveMod.Split[?, ?],
|
mod: LiveDyn.Split[?, ?],
|
||||||
includeUnchanged: Boolean
|
includeUnchanged: Boolean
|
||||||
): Diff.Split =
|
): Diff.Split =
|
||||||
Diff.Split(
|
Diff.Split(
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,20 @@ object HtmlBuilder:
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
static: Seq[String],
|
static: Seq[String],
|
||||||
dynamic: Seq[LiveMod[?]],
|
dynamic: Seq[LiveDyn[?]],
|
||||||
strw: StringWriter
|
strw: StringWriter
|
||||||
): Unit =
|
): Unit =
|
||||||
for i <- dynamic.indices do
|
for i <- dynamic.indices do
|
||||||
strw.append(static(i))
|
strw.append(static(i))
|
||||||
dynamic(i) match
|
dynamic(i) match
|
||||||
case mod: LiveMod.Dynamic[?, ?] =>
|
case mod: LiveDyn.Value[?, ?] =>
|
||||||
strw.append(mod.currentValue.toString)
|
strw.append(mod.currentValue.toString)
|
||||||
case mod: LiveMod.When[?] => build(mod, strw)
|
case mod: LiveDyn.When[?] => build(mod, strw)
|
||||||
case mod: LiveMod.Split[?, ?] => build(mod, strw)
|
case mod: LiveDyn.Split[?, ?] => build(mod, strw)
|
||||||
strw.append(static.last)
|
strw.append(static.last)
|
||||||
|
|
||||||
private def build(mod: LiveMod.When[?], strw: StringWriter): Unit =
|
private def build(mod: LiveDyn.When[?], strw: StringWriter): Unit =
|
||||||
if mod.displayed then build(mod.nested.static, mod.nested.dynamic, strw)
|
if mod.displayed then build(mod.nested.static, mod.nested.dynamic, strw)
|
||||||
|
|
||||||
private def build(mod: LiveMod.Split[?, ?], strw: StringWriter): Unit =
|
private def build(mod: LiveDyn.Split[?, ?], strw: StringWriter): Unit =
|
||||||
mod.dynamic.foreach(entry => build(mod.static, entry, strw))
|
mod.dynamic.foreach(entry => build(mod.static, entry, strw))
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ package scalive
|
||||||
import scala.collection.immutable.ArraySeq
|
import scala.collection.immutable.ArraySeq
|
||||||
import scala.collection.mutable.ArrayBuffer
|
import scala.collection.mutable.ArrayBuffer
|
||||||
|
|
||||||
sealed trait LiveMod[Model]:
|
sealed trait LiveDyn[Model]:
|
||||||
def update(model: Model): Unit
|
def update(model: Model): Unit
|
||||||
def wasUpdated: Boolean
|
def wasUpdated: Boolean
|
||||||
|
|
||||||
object LiveMod:
|
object LiveDyn:
|
||||||
|
|
||||||
class Dynamic[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false)
|
class Value[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false)
|
||||||
extends LiveMod[I]:
|
extends LiveDyn[I]:
|
||||||
private var value: O = d.run(init)
|
private var value: O = d.run(init)
|
||||||
private var updated: Boolean = startsUpdated
|
private var updated: Boolean = startsUpdated
|
||||||
def wasUpdated: Boolean = updated
|
def wasUpdated: Boolean = updated
|
||||||
|
|
@ -24,11 +24,11 @@ object LiveMod:
|
||||||
|
|
||||||
class When[Model](
|
class When[Model](
|
||||||
dynCond: Dyn[Model, Boolean],
|
dynCond: Dyn[Model, Boolean],
|
||||||
tag: HtmlTag[Model],
|
el: HtmlElement[Model],
|
||||||
init: Model
|
init: Model
|
||||||
) extends LiveMod[Model]:
|
) extends LiveDyn[Model]:
|
||||||
val cond = LiveMod.Dynamic(dynCond, init)
|
val cond = LiveDyn.Value(dynCond, init)
|
||||||
val nested = LiveView.render(tag, init)
|
val nested = LiveView.render(el, init)
|
||||||
def displayed: Boolean = cond.currentValue
|
def displayed: Boolean = cond.currentValue
|
||||||
def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated
|
def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated
|
||||||
def update(model: Model): Unit =
|
def update(model: Model): Unit =
|
||||||
|
|
@ -37,13 +37,16 @@ object LiveMod:
|
||||||
|
|
||||||
class Split[Model, Item](
|
class Split[Model, Item](
|
||||||
dynList: Dyn[Model, List[Item]],
|
dynList: Dyn[Model, List[Item]],
|
||||||
project: Dyn[Item, Item] => HtmlTag[Item],
|
project: Dyn[Item, Item] => HtmlElement[Item],
|
||||||
init: Model
|
init: Model
|
||||||
) extends LiveMod[Model]:
|
) extends LiveDyn[Model]:
|
||||||
private val tag = project(Dyn.id)
|
private val el = project(Dyn.id)
|
||||||
val static: ArraySeq[String] = LiveView.buildStatic(tag)
|
val static: ArraySeq[String] = LiveView.buildStatic(el)
|
||||||
val dynamic: ArrayBuffer[ArraySeq[LiveMod[Item]]] =
|
val dynamic: ArrayBuffer[ArraySeq[LiveDyn[Item]]] =
|
||||||
dynList.run(init).map(LiveView.buildDynamic(tag, _)).to(ArrayBuffer)
|
dynList
|
||||||
|
.run(init)
|
||||||
|
.map(LiveView.buildDynamic(el, _).to(ArraySeq))
|
||||||
|
.to(ArrayBuffer)
|
||||||
var removedIndexes: Seq[Int] = Seq.empty
|
var removedIndexes: Seq[Int] = Seq.empty
|
||||||
|
|
||||||
def wasUpdated: Boolean =
|
def wasUpdated: Boolean =
|
||||||
|
|
@ -57,6 +60,8 @@ object LiveMod:
|
||||||
dynamic.takeInPlace(items.size)
|
dynamic.takeInPlace(items.size)
|
||||||
items.zipWithIndex.map((item, i) =>
|
items.zipWithIndex.map((item, i) =>
|
||||||
if i >= dynamic.size then
|
if i >= dynamic.size then
|
||||||
dynamic.append(LiveView.buildDynamic(tag, item, startsUpdated = true))
|
dynamic.append(
|
||||||
|
LiveView.buildDynamic(el, item, startsUpdated = true).to(ArraySeq)
|
||||||
|
)
|
||||||
else dynamic(i).foreach(_.update(item))
|
else dynamic(i).foreach(_.update(item))
|
||||||
)
|
)
|
||||||
|
|
@ -6,7 +6,7 @@ import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
class LiveView[Model] private (
|
class LiveView[Model] private (
|
||||||
val static: ArraySeq[String],
|
val static: ArraySeq[String],
|
||||||
val dynamic: ArraySeq[LiveMod[Model]]
|
val dynamic: ArraySeq[LiveDyn[Model]]
|
||||||
):
|
):
|
||||||
def update(model: Model): Unit =
|
def update(model: Model): Unit =
|
||||||
dynamic.foreach(_.update(model))
|
dynamic.foreach(_.update(model))
|
||||||
|
|
@ -21,65 +21,79 @@ class LiveView[Model] private (
|
||||||
|
|
||||||
object LiveView:
|
object LiveView:
|
||||||
|
|
||||||
def apply[Model](
|
inline def apply[Model](
|
||||||
lv: View[Model],
|
lv: View[Model],
|
||||||
model: Model
|
model: Model
|
||||||
): LiveView[Model] =
|
): LiveView[Model] =
|
||||||
render(lv.view, model)
|
render(lv.root, model)
|
||||||
|
|
||||||
def buildStatic[Model](tag: HtmlTag[Model]): ArraySeq[String] =
|
def render[Model](
|
||||||
buildNestedStatic(tag).flatten.to(ArraySeq)
|
tag: HtmlElement[Model],
|
||||||
|
model: Model
|
||||||
|
): LiveView[Model] =
|
||||||
|
new LiveView(buildStatic(tag), buildDynamic(tag, model).to(ArraySeq))
|
||||||
|
|
||||||
private def buildNestedStatic[Model](
|
def buildStatic[Model](el: HtmlElement[Model]): ArraySeq[String] =
|
||||||
tag: HtmlTag[Model]
|
buildStaticFragments(el).flatten.to(ArraySeq)
|
||||||
|
|
||||||
|
private def buildStaticFragments[Model](
|
||||||
|
el: HtmlElement[Model]
|
||||||
): Seq[Option[String]] =
|
): Seq[Option[String]] =
|
||||||
|
val (attrs, children) = buildStaticFragmentsByType(el)
|
||||||
val static = ListBuffer.empty[Option[String]]
|
val static = ListBuffer.empty[Option[String]]
|
||||||
var staticFragment = s"<${tag.name}>"
|
var staticFragment = s"<${el.tag.name}"
|
||||||
for mod <- tag.mods.flatMap(buildStatic) do
|
for attr <- attrs do
|
||||||
mod match
|
attr match
|
||||||
case Some(s) =>
|
case Some(s) =>
|
||||||
staticFragment += s
|
staticFragment += s
|
||||||
case None =>
|
case None =>
|
||||||
static.append(Some(staticFragment))
|
static.append(Some(staticFragment))
|
||||||
static.append(None)
|
static.append(None)
|
||||||
staticFragment = ""
|
staticFragment = ""
|
||||||
staticFragment += s"</${tag.name}>"
|
staticFragment += s">"
|
||||||
|
for child <- children do
|
||||||
|
child match
|
||||||
|
case Some(s) =>
|
||||||
|
staticFragment += s
|
||||||
|
case None =>
|
||||||
|
static.append(Some(staticFragment))
|
||||||
|
static.append(None)
|
||||||
|
staticFragment = ""
|
||||||
|
staticFragment += s"</${el.tag.name}>"
|
||||||
static.append(Some(staticFragment))
|
static.append(Some(staticFragment))
|
||||||
static.toSeq
|
static.toSeq
|
||||||
|
|
||||||
def buildStatic[Model](mod: Mod[Model]): Seq[Option[String]] =
|
@nowarn("cat=unchecked")
|
||||||
mod match
|
private def buildStaticFragmentsByType[Model](
|
||||||
case Mod.Tag(tag) => buildNestedStatic(tag)
|
el: HtmlElement[Model]
|
||||||
case Mod.Text(text) => List(Some(text))
|
): (attrs: Seq[Option[String]], children: Seq[Option[String]]) =
|
||||||
case Mod.DynText(_) => List(None)
|
val (attrs, children) = el.mods.partitionMap {
|
||||||
case Mod.When(_, _) => List(None)
|
case Mod.StaticAttr(attr, value) =>
|
||||||
case Mod.Split(_, _) => List(None)
|
Left(List(Some(s""" ${attr.name}="$value"""")))
|
||||||
|
case Mod.Tag(el) => Right(buildStaticFragments(el))
|
||||||
def buildDynamic[Model](
|
case Mod.Text(text) => Right(List(Some(text)))
|
||||||
tag: HtmlTag[Model],
|
case Mod.DynText[Model](_) => Right(List(None))
|
||||||
model: Model,
|
case Mod.When[Model](_, _) => Right(List(None))
|
||||||
startsUpdated: Boolean = false
|
case Mod.Split[Model, Any](_, _) => Right(List(None))
|
||||||
): ArraySeq[LiveMod[Model]] =
|
}
|
||||||
tag.mods.flatMap(buildDynamic(_, model, startsUpdated)).to(ArraySeq)
|
(attrs.flatten, children.flatten)
|
||||||
|
|
||||||
@nowarn("cat=unchecked")
|
@nowarn("cat=unchecked")
|
||||||
def buildDynamic[Model](
|
def buildDynamic[Model](
|
||||||
mod: Mod[Model],
|
el: HtmlElement[Model],
|
||||||
model: Model,
|
model: Model,
|
||||||
startsUpdated: Boolean
|
startsUpdated: Boolean = false
|
||||||
): Seq[LiveMod[Model]] =
|
): Seq[LiveDyn[Model]] =
|
||||||
mod match
|
val (attrs, children) = el.mods.partitionMap {
|
||||||
case Mod.Tag(tag) => buildDynamic(tag, model, startsUpdated)
|
case Mod.StaticAttr(_, _) => Left(List.empty)
|
||||||
case Mod.Text(text) => List.empty
|
case Mod.Text(_) => Right(List.empty)
|
||||||
|
case Mod.Tag(el) =>
|
||||||
|
Right(buildDynamic(el, model, startsUpdated))
|
||||||
case Mod.DynText[Model](dynText) =>
|
case Mod.DynText[Model](dynText) =>
|
||||||
List(LiveMod.Dynamic(dynText, model, startsUpdated))
|
Right(List(LiveDyn.Value(dynText, model, startsUpdated)))
|
||||||
case Mod.When[Model](dynCond, tag) =>
|
case Mod.When[Model](dynCond, el) =>
|
||||||
List(LiveMod.When(dynCond, tag, model))
|
Right(List(LiveDyn.When(dynCond, el, model)))
|
||||||
case Mod.Split[Model, Any](dynList, project) =>
|
case Mod.Split[Model, Any](dynList, project) =>
|
||||||
List(LiveMod.Split(dynList, project, model))
|
Right(List(LiveDyn.Split(dynList, project, model)))
|
||||||
|
}
|
||||||
def render[Model](
|
attrs.flatten ++ children.flatten
|
||||||
tag: HtmlTag[Model],
|
|
||||||
model: Model
|
|
||||||
): LiveView[Model] =
|
|
||||||
new LiveView(buildStatic(tag), buildDynamic(tag, model))
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ package scalive
|
||||||
|
|
||||||
trait View[Model]:
|
trait View[Model]:
|
||||||
val model: Dyn[Model, Model] = Dyn.id
|
val model: Dyn[Model, Model] = Dyn.id
|
||||||
def view: HtmlTag[Model]
|
def root: HtmlElement[Model]
|
||||||
|
|
||||||
opaque type Dyn[I, O] = I => O
|
opaque type Dyn[I, O] = I => O
|
||||||
extension [I, O](d: Dyn[I, O])
|
extension [I, O](d: Dyn[I, O])
|
||||||
def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f)
|
def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f)
|
||||||
|
|
||||||
def when(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] =
|
def when(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] =
|
||||||
Mod.When(d.andThen(f), tag)
|
Mod.When(d.andThen(f), el)
|
||||||
|
|
||||||
inline def whenNot(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] =
|
inline def whenNot(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] =
|
||||||
when(f.andThen(!_))(tag)
|
when(f.andThen(!_))(el)
|
||||||
|
|
||||||
def splitByIndex[O2](f: O => List[O2])(
|
def splitByIndex[O2](f: O => List[O2])(
|
||||||
project: Dyn[O2, O2] => HtmlTag[O2]
|
project: Dyn[O2, O2] => HtmlElement[O2]
|
||||||
): Mod.Split[I, O2] =
|
): Mod.Split[I, O2] =
|
||||||
Mod.Split(d.andThen(f), project)
|
Mod.Split(d.andThen(f), project)
|
||||||
|
|
||||||
|
|
@ -25,26 +25,31 @@ object Dyn:
|
||||||
def id[T]: Dyn[T, T] = identity
|
def id[T]: Dyn[T, T] = identity
|
||||||
|
|
||||||
enum Mod[T]:
|
enum Mod[T]:
|
||||||
case Tag(tag: HtmlTag[T])
|
case StaticAttr(attr: HtmlAttr, value: String)
|
||||||
|
case Tag(el: HtmlElement[T])
|
||||||
case Text(text: String)
|
case Text(text: String)
|
||||||
case DynText(dynText: Dyn[T, String])
|
case DynText(dynText: Dyn[T, String])
|
||||||
case When(dynCond: Dyn[T, Boolean], tag: HtmlTag[T])
|
case When(dynCond: Dyn[T, Boolean], el: HtmlElement[T])
|
||||||
case Split[T, O](
|
case Split[T, O](
|
||||||
dynList: Dyn[T, List[O]],
|
dynList: Dyn[T, List[O]],
|
||||||
project: Dyn[O, O] => HtmlTag[O]
|
project: Dyn[O, O] => HtmlElement[O]
|
||||||
) extends Mod[T]
|
) extends Mod[T]
|
||||||
|
|
||||||
given [T]: Conversion[HtmlTag[T], Mod[T]] = Mod.Tag(_)
|
given [T]: Conversion[HtmlElement[T], Mod[T]] = Mod.Tag(_)
|
||||||
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
|
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
|
||||||
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
|
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
|
||||||
|
|
||||||
trait HtmlTag[Model](val name: String):
|
class HtmlTag(val name: String):
|
||||||
def mods: List[Mod[Model]]
|
def apply[Model](mods: Mod[Model]*): HtmlElement[Model] =
|
||||||
|
HtmlElement(this, mods.toVector)
|
||||||
|
|
||||||
class Div[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("div")
|
class HtmlElement[Model](val tag: HtmlTag, val mods: Vector[Mod[Model]])
|
||||||
class Ul[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("ul")
|
|
||||||
class Li[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("li")
|
|
||||||
|
|
||||||
def div[Model](mods: Mod[Model]*): Div[Model] = Div(mods.toList)
|
val div = HtmlTag("div")
|
||||||
def ul[Model](mods: Mod[Model]*): Ul[Model] = Ul(mods.toList)
|
val ul = HtmlTag("ul")
|
||||||
def li[Model](mods: Mod[Model]*): Li[Model] = Li(mods.toList)
|
val li = HtmlTag("li")
|
||||||
|
|
||||||
|
class HtmlAttr(val name: String):
|
||||||
|
def :=[T](value: String): Mod.StaticAttr[T] = Mod.StaticAttr(this, value)
|
||||||
|
|
||||||
|
val idAttr = HtmlAttr("id")
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ final case class MyModel(elems: List[NestedModel])
|
||||||
final case class NestedModel(name: String, age: Int)
|
final case class NestedModel(name: String, age: Int)
|
||||||
|
|
||||||
object TestView extends View[MyModel]:
|
object TestView extends View[MyModel]:
|
||||||
val view: HtmlTag[MyModel] =
|
val root: HtmlElement[MyModel] =
|
||||||
div(
|
div(
|
||||||
|
idAttr := "42",
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.elems)(elem =>
|
model.splitByIndex(_.elems)(elem =>
|
||||||
li(
|
li(
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
val lv =
|
val lv =
|
||||||
LiveView(
|
LiveView(
|
||||||
new View[Unit]:
|
new View[Unit]:
|
||||||
val view: HtmlTag[Unit] =
|
val root: HtmlElement[Unit] =
|
||||||
div("Static string")
|
div("Static string")
|
||||||
,
|
,
|
||||||
()
|
()
|
||||||
|
|
@ -47,7 +47,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
val lv =
|
val lv =
|
||||||
LiveView(
|
LiveView(
|
||||||
new View[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val root: HtmlElement[TestModel] =
|
||||||
div(model(_.title))
|
div(model(_.title))
|
||||||
,
|
,
|
||||||
TestModel()
|
TestModel()
|
||||||
|
|
@ -83,7 +83,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
val lv =
|
val lv =
|
||||||
LiveView(
|
LiveView(
|
||||||
new View[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val root: HtmlElement[TestModel] =
|
||||||
div(
|
div(
|
||||||
model.when(_.bool)(
|
model.when(_.bool)(
|
||||||
div("static string", model(_.nestedTitle))
|
div("static string", model(_.nestedTitle))
|
||||||
|
|
@ -152,7 +152,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
val lv =
|
val lv =
|
||||||
LiveView(
|
LiveView(
|
||||||
new View[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val root: HtmlElement[TestModel] =
|
||||||
div(
|
div(
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.items)(elem =>
|
model.splitByIndex(_.items)(elem =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue